This repository has been archived on 2026-03-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Timmy-time-dashboard/tests/dashboard/test_calm.py
Alexander Whitestone 9d78eb31d1 ruff (#169)
* polish: streamline nav, extract inline styles, improve tablet UX

- Restructure desktop nav from 8+ flat links + overflow dropdown into
  5 grouped dropdowns (Core, Agents, Intel, System, More) matching
  the mobile menu structure to reduce decision fatigue
- Extract all inline styles from mission_control.html and base.html
  notification elements into mission-control.css with semantic classes
- Replace JS-built innerHTML with secure DOM construction in
  notification loader and chat history
- Add CONNECTING state to connection indicator (amber) instead of
  showing OFFLINE before WebSocket connects
- Add tablet breakpoint (1024px) with larger touch targets for
  Apple Pencil / stylus use and safe-area padding for iPad toolbar
- Add active-link highlighting in desktop dropdown menus
- Rename "Mission Control" page title to "System Overview" to
  disambiguate from the chat home page
- Add "Home — Timmy Time" page title to index.html

https://claude.ai/code/session_015uPUoKyYa8M2UAcyk5Gt6h

* fix(security): move auth-gate credentials to environment variables

Hardcoded username, password, and HMAC secret in auth-gate.py replaced
with os.environ lookups. Startup now refuses to run if any variable is
unset. Added AUTH_GATE_SECRET/USER/PASS to .env.example.

https://claude.ai/code/session_015uPUoKyYa8M2UAcyk5Gt6h

* refactor(tooling): migrate from black+isort+bandit to ruff

Replace three separate linting/formatting tools with a single ruff
invocation. Updates tox.ini (lint, format, pre-push, pre-commit envs),
.pre-commit-config.yaml, and CI workflow. Fixes all ruff errors
including unused imports, missing raise-from, and undefined names.
Ruff config maps existing bandit skips to equivalent S-rules.

https://claude.ai/code/session_015uPUoKyYa8M2UAcyk5Gt6h

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-11 12:23:35 -04:00

257 lines
11 KiB
Python

from datetime import date
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import Session, sessionmaker
from sqlalchemy.pool import StaticPool
from dashboard.app import app
from dashboard.models.calm import JournalEntry, Task, TaskCertainty, TaskState
from dashboard.models.database import Base, get_db
@pytest.fixture(name="test_db_engine")
def test_db_engine_fixture():
# Use StaticPool to keep the in-memory database alive across multiple connections
engine = create_engine(
"sqlite:///:memory:", connect_args={"check_same_thread": False}, poolclass=StaticPool
)
Base.metadata.create_all(bind=engine) # Create tables
yield engine
Base.metadata.drop_all(bind=engine) # Drop tables after test
@pytest.fixture(name="db_session")
def db_session_fixture(test_db_engine):
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=test_db_engine)
db = TestingSessionLocal()
try:
yield db
finally:
db.close()
@pytest.fixture(name="client")
def client_fixture(db_session: Session):
app.dependency_overrides[get_db] = lambda: db_session
with TestClient(app) as client:
yield client
app.dependency_overrides.clear()
def test_create_task(client: TestClient, db_session: Session):
response = client.post(
"/calm/tasks",
data={
"title": "Test Task",
"description": "This is a test description",
"is_mit": False,
"certainty": TaskCertainty.SOFT.value,
},
)
assert response.status_code == 200
# The actual ID in the template is later-count-container
assert "later-count-container" in response.text
task = db_session.query(Task).filter(Task.title == "Test Task").first()
assert task is not None
assert task.state == TaskState.LATER
assert task.description == "This is a test description"
def test_morning_ritual_creates_tasks_and_journal_entry(client: TestClient, db_session: Session):
response = client.post(
"/calm/ritual/morning",
data={
"mit1_title": "MIT Task 1",
"mit2_title": "MIT Task 2",
"other_tasks": "Other Task 1\nOther Task 2",
},
)
assert response.status_code == 200
assert "Timmy Calm" in response.text
journal_entry = db_session.query(JournalEntry).first()
assert journal_entry is not None
assert len(journal_entry.mit_task_ids) == 2
tasks = db_session.query(Task).all()
assert len(tasks) == 4
mit_tasks = db_session.query(Task).filter(Task.is_mit).all()
assert len(mit_tasks) == 2
now_task = db_session.query(Task).filter(Task.state == TaskState.NOW).first()
next_task = db_session.query(Task).filter(Task.state == TaskState.NEXT).first()
later_tasks = db_session.query(Task).filter(Task.state == TaskState.LATER).all()
assert now_task is not None
assert next_task is not None
assert len(later_tasks) == 2
def test_complete_now_task_promotes_next_and_later(client: TestClient, db_session: Session):
task_now = Task(title="Task NOW", state=TaskState.NOW, is_mit=True, sort_order=0)
task_next = Task(title="Task NEXT", state=TaskState.NEXT, is_mit=False, sort_order=0)
task_later1 = Task(title="Task LATER 1", state=TaskState.LATER, is_mit=True, sort_order=0)
task_later2 = Task(title="Task LATER 2", state=TaskState.LATER, is_mit=False, sort_order=1)
db_session.add_all([task_now, task_next, task_later1, task_later2])
db_session.commit()
db_session.refresh(task_now)
db_session.refresh(task_next)
db_session.refresh(task_later1)
db_session.refresh(task_later2)
response = client.post(f"/calm/tasks/{task_now.id}/complete")
assert response.status_code == 200
assert db_session.query(Task).filter(Task.id == task_now.id).first().state == TaskState.DONE
assert db_session.query(Task).filter(Task.id == task_next.id).first().state == TaskState.NOW
assert db_session.query(Task).filter(Task.id == task_later1.id).first().state == TaskState.NEXT
assert db_session.query(Task).filter(Task.id == task_later2.id).first().state == TaskState.LATER
def test_defer_now_task_promotes_next_and_later(client: TestClient, db_session: Session):
task_now = Task(title="Task NOW", state=TaskState.NOW, is_mit=True, sort_order=0)
task_next = Task(title="Task NEXT", state=TaskState.NEXT, is_mit=False, sort_order=0)
task_later1 = Task(title="Task LATER 1", state=TaskState.LATER, is_mit=True, sort_order=0)
task_later2 = Task(title="Task LATER 2", state=TaskState.LATER, is_mit=False, sort_order=1)
db_session.add_all([task_now, task_next, task_later1, task_later2])
db_session.commit()
db_session.refresh(task_now)
db_session.refresh(task_next)
db_session.refresh(task_later1)
db_session.refresh(task_later2)
response = client.post(f"/calm/tasks/{task_now.id}/defer")
assert response.status_code == 200
assert db_session.query(Task).filter(Task.id == task_now.id).first().state == TaskState.DEFERRED
assert db_session.query(Task).filter(Task.id == task_next.id).first().state == TaskState.NOW
assert db_session.query(Task).filter(Task.id == task_later1.id).first().state == TaskState.NEXT
assert db_session.query(Task).filter(Task.id == task_later2.id).first().state == TaskState.LATER
def test_start_task_demotes_current_now_and_promotes_to_now(
client: TestClient, db_session: Session
):
task_now = Task(title="Task NOW", state=TaskState.NOW, is_mit=True, sort_order=0)
task_next = Task(title="Task NEXT", state=TaskState.NEXT, is_mit=False, sort_order=0)
task_later1 = Task(title="Task LATER 1", state=TaskState.LATER, is_mit=True, sort_order=0)
db_session.add_all([task_now, task_next, task_later1])
db_session.commit()
db_session.refresh(task_now)
db_session.refresh(task_next)
db_session.refresh(task_later1)
response = client.post(f"/calm/tasks/{task_later1.id}/start")
assert response.status_code == 200
assert db_session.query(Task).filter(Task.id == task_later1.id).first().state == TaskState.NOW
assert db_session.query(Task).filter(Task.id == task_now.id).first().state == TaskState.NEXT
# According to promote_tasks logic, if NEXT exists, it stays NEXT.
assert db_session.query(Task).filter(Task.id == task_next.id).first().state == TaskState.NEXT
def test_evening_ritual_archives_active_tasks(client: TestClient, db_session: Session):
journal_entry = JournalEntry(entry_date=date.today())
db_session.add(journal_entry)
db_session.commit()
db_session.refresh(journal_entry)
task_now = Task(title="Task NOW", state=TaskState.NOW)
task_next = Task(title="Task NEXT", state=TaskState.NEXT)
task_later = Task(title="Task LATER", state=TaskState.LATER)
task_done = Task(title="Task DONE", state=TaskState.DONE)
db_session.add_all([task_now, task_next, task_later, task_done])
db_session.commit()
response = client.post(
"/calm/ritual/evening",
data={
"evening_reflection": "Reflected well",
"gratitude": "Grateful for everything",
"energy_level": 8,
},
)
assert response.status_code == 200
assert "Evening Ritual Complete" in response.text
assert db_session.query(Task).filter(Task.id == task_now.id).first().state == TaskState.DEFERRED
assert (
db_session.query(Task).filter(Task.id == task_next.id).first().state == TaskState.DEFERRED
)
assert (
db_session.query(Task).filter(Task.id == task_later.id).first().state == TaskState.DEFERRED
)
assert db_session.query(Task).filter(Task.id == task_done.id).first().state == TaskState.DONE
updated_journal = (
db_session.query(JournalEntry).filter(JournalEntry.id == journal_entry.id).first()
)
assert updated_journal.evening_reflection == "Reflected well"
assert updated_journal.gratitude == "Grateful for everything"
assert updated_journal.energy_level == 8
def test_reorder_later_tasks(client: TestClient, db_session: Session):
task_later1 = Task(title="Task LATER 1", state=TaskState.LATER, sort_order=0)
task_later2 = Task(title="Task LATER 2", state=TaskState.LATER, sort_order=1)
task_later3 = Task(title="Task LATER 3", state=TaskState.LATER, sort_order=2)
db_session.add_all([task_later1, task_later2, task_later3])
db_session.commit()
db_session.refresh(task_later1)
db_session.refresh(task_later2)
db_session.refresh(task_later3)
response = client.post(
"/calm/tasks/reorder",
data={"later_task_ids": f"{task_later3.id},{task_later1.id},{task_later2.id}"},
)
assert response.status_code == 200
assert db_session.query(Task).filter(Task.id == task_later3.id).first().sort_order == 0
assert db_session.query(Task).filter(Task.id == task_later1.id).first().sort_order == 1
assert db_session.query(Task).filter(Task.id == task_later2.id).first().sort_order == 2
def test_reorder_promote_later_to_next(client: TestClient, db_session: Session):
task_now = Task(title="Task NOW", state=TaskState.NOW, is_mit=True, sort_order=0)
task_later1 = Task(title="Task LATER 1", state=TaskState.LATER, is_mit=False, sort_order=0)
task_later2 = Task(title="Task LATER 2", state=TaskState.LATER, is_mit=False, sort_order=1)
db_session.add_all([task_now, task_later1, task_later2])
db_session.commit()
db_session.refresh(task_now)
db_session.refresh(task_later1)
db_session.refresh(task_later2)
response = client.post(
"/calm/tasks/reorder",
data={"next_task_id": task_later1.id},
)
assert response.status_code == 200
assert db_session.query(Task).filter(Task.id == task_now.id).first().state == TaskState.NOW
assert db_session.query(Task).filter(Task.id == task_later1.id).first().state == TaskState.NEXT
assert db_session.query(Task).filter(Task.id == task_later2.id).first().state == TaskState.LATER
def test_create_tables_idempotent_under_concurrency():
"""Calling create_tables() when tables already exist must not crash.
This covers the race where multiple pytest-xdist workers (or app
processes) import the calm routes module simultaneously and each
calls create_tables() against the same SQLite file.
"""
from unittest.mock import patch
from sqlalchemy.exc import OperationalError
from dashboard.models.database import create_tables
fake_error = OperationalError("CREATE TABLE", {}, Exception("table tasks already exists"))
with patch("dashboard.models.database.Base.metadata.create_all", side_effect=fake_error):
# Must not raise — the OperationalError is caught and logged
create_tables()