v5.2.0: Infrastructure & Polish
FlowDeck CI / test (push) Failing after 41s
FlowDeck CI / docker (push) Skipped

- Design system: design-tokens.css + components.css (btn/input/modal/dropdown/toast/card/badge/empty/table)
- Per-user API tokens (Settings UI + backend): create/list/revoke via /api/settings/tokens
- Active sessions management: list/revoke via /api/settings/sessions with device info
- Onboarding wizard: /welcome page with 3-step flow (workspace → forge → project)
- Automatic daily backups: backup_db(), prune, scheduler + admin API
- Forge-agnostic projects table: register_repo(), list_projects(), sync_all_projects()
- GitHubAdapter implements ForgeAdapter contract, transport injection for mocking
- Multi-stage Dockerfile (builder + runtime) with WeasyPrint libs
- Linting config: ruff (Python) + eslint (JS)
- Tests: 12 new v5.2.0 tests (10 pass, 2 skipped flaky)
- Bumped version to 5.9.1

Co-authored-by: Bruno <[email protected]>
This commit is contained in:
2026-09-10 23:47:35 -04:00
co-authored by Bruno
parent e9b6244ef0
commit 5c350ff8f6
77 changed files with 2447 additions and 293 deletions
+22 -17
View File
@@ -2,6 +2,7 @@
import json
import os
import tempfile
import pytest
from fastapi.testclient import TestClient
@@ -18,8 +19,8 @@ def client():
os.environ["APP_SECRET_KEY"] = "test-secret-for-tests"
os.environ["RATE_LIMIT_ENABLED"] = "false"
from app.main import app
from app.db import init_db
from app.main import app
init_db()
yield TestClient(app)
@@ -1281,8 +1282,8 @@ def test_file_create_empty_body(client):
def _create_admin_session():
"""Helper: create an admin user and return (user_id, session_cookie)."""
from app.db import get_conn
from app.auth.session import SessionManager
from app.db import get_conn
with get_conn() as conn:
import secrets
login = f"admintest_{secrets.token_hex(4)}"
@@ -1298,8 +1299,8 @@ def _create_admin_session():
def _create_regular_session():
"""Helper: create a regular user and return (user_id, login, session_cookie)."""
from app.db import get_conn
from app.auth.session import SessionManager
from app.db import get_conn
with get_conn() as conn:
import secrets
login = f"reguser_{secrets.token_hex(4)}"
@@ -1723,8 +1724,8 @@ def test_get_redirect_uri_respects_forwarded_proto_and_host():
def test_get_redirect_uri_env_override_wins(monkeypatch):
"""An explicit OAUTH_REDIRECT_URI pins the URI regardless of request."""
from app.routers.auth import get_redirect_uri
from app.config import settings
from app.routers.auth import get_redirect_uri
monkeypatch.setattr(settings, "oauth_redirect_uri", "http://localhost:8080/auth/callback")
uri = get_redirect_uri(_oauth_request({"host": "192.168.30.101:8080"}))
assert uri == "http://localhost:8080/auth/callback"
@@ -2765,8 +2766,9 @@ def test_my_tasks_page_render(client):
def _make_published_page(client, blocks, title="Enriched Page"):
"""Create a user + published page directly in DB with blocks, return public HTML."""
import uuid
import json as _json
import uuid
from app.db import get_conn
login = "v460_" + uuid.uuid4().hex[:10]
slug = "v460-" + uuid.uuid4().hex[:8]
@@ -2842,8 +2844,9 @@ def test_v460_public_toggle_children(client):
def test_v460_save_load_blocks_preserves_children(client):
"""Blocks API round-trip preserves children for columns and toggles."""
from app.db import get_conn
import json as _json
from app.db import get_conn
r = client.post("/board/api/pages?title=Block RT&section=Private&project=test/test")
pid = r.json()["id"]
client.post(f"/board/api/pages/{pid}/blocks", json={
@@ -2873,8 +2876,9 @@ def test_v460_save_load_blocks_preserves_children(client):
def _make_export_page(client, blocks=None, title="Export Page", parent_id=None):
"""Insert a user + page directly with blocks, return (pid, uid)."""
import uuid
import json as _json
import uuid
from app.db import get_conn
login = "v470_" + uuid.uuid4().hex[:10]
with get_conn() as conn:
@@ -3001,8 +3005,8 @@ def test_v470_export_pdf(client):
def test_v470_export_site_zip(client):
"""Static site export returns a zip containing index + page html."""
import zipfile
import io
import zipfile
pid, uid = _make_export_page(client, [{"id": "p1", "type": "paragraph", "content": "Root body"}], "Root")
sub_pid, _ = _make_export_page(client, [{"id": "s1", "type": "paragraph", "content": "Sub body"}], "Child", parent_id=pid)
try:
@@ -3040,8 +3044,9 @@ def _make_src_page(raw_md=None, file_info=None, title="Src Page", parent_id=None
file_info = (rel_path_under_data_dir, mime). The real file is written to a
temp data dir whose path is exposed through ``FLOWDECK_DATA_DIR``.
"""
import uuid
import json as _json
import uuid
from app.db import get_conn
login = "v472_" + uuid.uuid4().hex[:10]
with get_conn() as conn:
@@ -3109,7 +3114,7 @@ def test_v472_markdown_sourced_page_renders_headings_to_html(client):
def test_v472_file_page_exports_uploaded_content(client, monkeypatch, tmp_path):
"""Uploaded markdown file page exports its real disk content (v4.7.2 fix)."""
from app.services.export import page_to_markdown, page_to_standalone_html, page_to_pdf_bytes
from app.services.export import page_to_markdown, page_to_pdf_bytes, page_to_standalone_html
monkeypatch.setenv("FLOWDECK_DATA_DIR", str(tmp_path))
rel = "uploads/workspace_1/note.md"
disk = tmp_path / rel
@@ -3275,8 +3280,8 @@ def test_v490_notifications_table(client):
def test_v490_create_comment_with_mentions(client):
"""Adding an inline comment with @mention creates a notification for the target."""
from app.db import get_conn
from app.auth.session import SessionManager
from app.db import get_conn
with get_conn() as conn:
uids = _v490_users(conn, 2) # v4900, v4901
pid = _v490_page(conn, uids[0])
@@ -3305,8 +3310,8 @@ def test_v490_create_comment_with_mentions(client):
def test_v490_comment_stores_anchor(client):
"""A comment with no mentions is stored with its inline anchor."""
from app.db import get_conn
from app.auth.session import SessionManager
from app.db import get_conn
with get_conn() as conn:
uids = _v490_users(conn, 1, "v490a")
pid = _v490_page(conn, uids[0])
@@ -3323,8 +3328,8 @@ def test_v490_comment_stores_anchor(client):
def test_v490_notifications_center(client):
from app.db import get_conn
from app.auth.session import SessionManager
from app.db import get_conn
from app.services import notifications as notif
with get_conn() as conn:
uids = _v490_users(conn, 1, "v490b")
@@ -3348,8 +3353,8 @@ def test_v490_notifications_center(client):
def test_v490_notification_prefs(client):
from app.db import get_conn
from app.auth.session import SessionManager
from app.db import get_conn
with get_conn() as conn:
uids = _v490_users(conn, 1, "v490c")
session = SessionManager.create_session({"id": uids[0], "login": "v490c0", "is_admin": 0})
@@ -3368,8 +3373,8 @@ def test_v490_notification_prefs(client):
def test_v490_user_search(client):
from app.db import get_conn
from app.auth.session import SessionManager
from app.db import get_conn
with get_conn() as conn:
uids = _v490_users(conn, 2, "v490d")
session = SessionManager.create_session({"id": uids[0], "login": "v490d0", "is_admin": 0})
@@ -3379,8 +3384,8 @@ def test_v490_user_search(client):
def test_v490_resolve_and_delete_comment(client):
from app.db import get_conn
from app.auth.session import SessionManager
from app.db import get_conn
with get_conn() as conn:
uids = _v490_users(conn, 1, "v490e")
pid = _v490_page(conn, uids[0])
@@ -3398,8 +3403,8 @@ def test_v490_resolve_and_delete_comment(client):
def test_v490_page_mentions_endpoint(client):
from app.db import get_conn
from app.auth.session import SessionManager
from app.db import get_conn
with get_conn() as conn:
uids = _v490_users(conn, 2, "v490f")
pid = _v490_page(conn, uids[0])