test: A32 phase 2a — library 10/10 + 2 routes fantômes supprimées (v7.12.0)
- tests/test_smoke_uncovered.py : +6 tests pour library.py (1/10 → 8 routes
couvertes) : les 5 listes en boucle (recents/favorites/published/private/
workspace → 200 + items), /private avec page seedée retrouvée, /children/{id}
avec parent/enfant seedés, /repository vide ET clé (string, aucun réseau),
non-régression 404 sur les routes supprimées.
- DÉCOUVERTE (les smokes l'ont prouvé) : `/api/library/local-workspace-children`
renvoyait un 500 systématique (test vert → ASGI double response.start) et
`/api/library/local-workspace` 500 dès qu'un workspace existe — les deux
lisaient `local_workspace_items`, table AUCUNEMENT créée dans le codebase
(0 CREATE TABLE) et sans 1 seule référence front. Supprimés avec
`library._format_size` devenu mort (la copie de dashboard.py est inchangée).
`local_workspace_items` : 0 occurrence restante dans app/.
- helper `_seed_page` minimal (workspace NOT NULL inclus) + nettoyage en
finally (pas de pollution des autres tests).
suite **1053/1053** (229 s) · `ruff check app tests` OK · docs à jour
restent phase 2b : api.py 3/23, dashboard.py 17/63, api_v2.py 50/115
This commit is contained in:
@@ -1,5 +1,31 @@
|
|||||||
# Changelog - FlowDeck
|
# Changelog - FlowDeck
|
||||||
|
|
||||||
|
## v7.12.0 (2026-10-01) — Audit : A32 phase 2a (library 10/10)
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
|
||||||
|
- `library.py` passe de **1/10 à 8 routes couvertes** : les 5 listes
|
||||||
|
(recents/favorites/published/private/workspace) en un test de boucle,
|
||||||
|
`/private` avec une page seedée et retrouvée, `/children/{id}` avec un
|
||||||
|
parent/enfant seedés (titre retrouvé), `/repository` vide et clé
|
||||||
|
(aucun appel réseau — la clé n'est qu'une string de workspace)
|
||||||
|
- Test de non-régression 404 sur les 2 routes supprimées
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- **2 routes cassées supprimées** (découverte des smokes) :
|
||||||
|
`/api/library/local-workspace-children/{id}` renvoyait un 500 systématique
|
||||||
|
et `/api/library/local-workspace` un 500 dès qu'un workspace existait —
|
||||||
|
les deux lisaient `local_workspace_items`, **une table qui n'est créée nulle
|
||||||
|
part** dans le codebase (grep : 0 `CREATE TABLE`), avec **0 référence front**.
|
||||||
|
`library._format_size` devenu mort : supprimé aussi (une version vit dans
|
||||||
|
`dashboard.py`, inchangée)
|
||||||
|
- `local_workspace_items` : plus aucune occurrence dans `app/`
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
|
||||||
|
- Suite complète : **1053/1053** (229 s) ; `test_smoke_uncovered.py` : 16 tests
|
||||||
|
|
||||||
## v7.11.0 (2026-10-01) — Audit : A32 phase 1 (routers à 0 test)
|
## v7.11.0 (2026-10-01) — Audit : A32 phase 1 (routers à 0 test)
|
||||||
|
|
||||||
### Tests
|
### Tests
|
||||||
|
|||||||
+2
-2
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
# WORKLOAD — FlowDeck Notion Clone
|
# WORKLOAD — FlowDeck Notion Clone
|
||||||
|
|
||||||
> **Début**: 2026-07-08 | **Version**: v7.11.0 (audit — A32 phase 1 : 4 routers à 0 test couverts) | **Statut**: EN COURS 🔄
|
> **Début**: 2026-07-08 | **Version**: v7.12.0 (audit — A32 phase 2 : library 10/10, 2 routes mortes supprimées) | **Statut**: EN COURS 🔄
|
||||||
> **Cible**: parité Notion + intégration forge · **Follow-ups v7.3 livrés**: sidebar teamspaces, notif `page.updated`, charts `number` + dashboards multi-DB, unfurl forge, UI Settings → Audit — voir `ROADMAP.md § v7.3.0`
|
> **Cible**: parité Notion + intégration forge · **Follow-ups v7.3 livrés**: sidebar teamspaces, notif `page.updated`, charts `number` + dashboards multi-DB, unfurl forge, UI Settings → Audit — voir `ROADMAP.md § v7.3.0`
|
||||||
|
|
||||||
## Avancement Global
|
## Avancement Global
|
||||||
|
|||||||
+1
-1
@@ -185,7 +185,7 @@ async def lifespan(_app: FastAPI):
|
|||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="FlowDeck",
|
title="FlowDeck",
|
||||||
version="7.11.0",
|
version="7.12.0",
|
||||||
docs_url="/docs",
|
docs_url="/docs",
|
||||||
redoc_url="/redoc",
|
redoc_url="/redoc",
|
||||||
lifespan=lifespan,
|
lifespan=lifespan,
|
||||||
|
|||||||
@@ -341,74 +341,6 @@ def library_private(
|
|||||||
return {"items": items}
|
return {"items": items}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/local-workspace-children/{item_id:int}")
|
|
||||||
def library_local_workspace_children(item_id: int, request: Request):
|
|
||||||
"""Return children of a local workspace item for tree expansion."""
|
|
||||||
_get_user_id(request)
|
|
||||||
with get_conn() as conn:
|
|
||||||
# Get the item to find its workspace
|
|
||||||
item = conn.execute(
|
|
||||||
"SELECT workspace_id FROM local_workspace_items WHERE id=? AND deleted_at IS NULL",
|
|
||||||
[item_id],
|
|
||||||
).fetchone()
|
|
||||||
if not item:
|
|
||||||
return {"items": []}
|
|
||||||
|
|
||||||
rows = conn.execute(
|
|
||||||
"SELECT id, name, is_folder, parent_id, content_format, size, "
|
|
||||||
"COALESCE(updated_at, created_at) as updated_at "
|
|
||||||
"FROM local_workspace_items "
|
|
||||||
"WHERE parent_id = ? AND deleted_at IS NULL "
|
|
||||||
"ORDER BY is_folder DESC, name COLLATE NOCASE",
|
|
||||||
[item_id],
|
|
||||||
).fetchall()
|
|
||||||
|
|
||||||
items = []
|
|
||||||
for r in rows:
|
|
||||||
name = r["name"] or "Untitled"
|
|
||||||
is_folder = bool(r["is_folder"])
|
|
||||||
icon = "📁" if is_folder else "📄"
|
|
||||||
fn = name.lower()
|
|
||||||
if not is_folder:
|
|
||||||
if fn.endswith(".pdf"):
|
|
||||||
icon = "📕"
|
|
||||||
elif any(fn.endswith(e) for e in [".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg"]):
|
|
||||||
icon = "🖼️"
|
|
||||||
elif any(fn.endswith(e) for e in [".py", ".js", ".ts", ".go", ".rs"]):
|
|
||||||
icon = "📜"
|
|
||||||
|
|
||||||
with get_conn() as conn:
|
|
||||||
child_count = conn.execute(
|
|
||||||
"SELECT COUNT(*) FROM local_workspace_items WHERE parent_id=? AND deleted_at IS NULL",
|
|
||||||
[r["id"]],
|
|
||||||
).fetchone()[0]
|
|
||||||
|
|
||||||
items.append({
|
|
||||||
"id": r["id"],
|
|
||||||
"title": name,
|
|
||||||
"icon": icon,
|
|
||||||
"is_folder": is_folder,
|
|
||||||
"source_type": "local-ws",
|
|
||||||
"source_label": "",
|
|
||||||
"workspace": "",
|
|
||||||
"workspace_name": "",
|
|
||||||
"author": "",
|
|
||||||
"author_initial": "?",
|
|
||||||
"updated_at": r["updated_at"] or "",
|
|
||||||
"visited_at": "",
|
|
||||||
"has_children": child_count > 0,
|
|
||||||
"children": [],
|
|
||||||
"url": f"/local-workspace?folder={r['id']}" if is_folder else f"/pages/{r['id']}",
|
|
||||||
"content_format": r["content_format"] or "file",
|
|
||||||
"favorited": False,
|
|
||||||
"page_icon": "",
|
|
||||||
"tags": [],
|
|
||||||
"size_display": _format_size(r["size"]) if r["size"] else "",
|
|
||||||
})
|
|
||||||
|
|
||||||
return {"items": items}
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/children/{page_id:int}")
|
@router.get("/children/{page_id:int}")
|
||||||
def library_children(page_id: int, request: Request):
|
def library_children(page_id: int, request: Request):
|
||||||
"""Return child pages for a given parent page (for tree expansion in Library)."""
|
"""Return child pages for a given parent page (for tree expansion in Library)."""
|
||||||
@@ -449,91 +381,6 @@ def library_repository(
|
|||||||
return {"items": items}
|
return {"items": items}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/local-workspace")
|
|
||||||
def library_local_workspace(
|
|
||||||
request: Request,
|
|
||||||
workspace_id: int = Query(default=0),
|
|
||||||
):
|
|
||||||
"""Return local workspace items (files/folders) formatted for Library display."""
|
|
||||||
from app.routers.dashboard import _get_active_workspace
|
|
||||||
uid = _get_user_id(request)
|
|
||||||
|
|
||||||
# Get the active workspace
|
|
||||||
ws = _get_active_workspace(request, user_id=uid)
|
|
||||||
if not ws:
|
|
||||||
return {"items": []}
|
|
||||||
|
|
||||||
ws_id = workspace_id or ws["id"]
|
|
||||||
|
|
||||||
# Query local workspace tree
|
|
||||||
with get_conn() as conn:
|
|
||||||
rows = conn.execute(
|
|
||||||
"SELECT id, name, is_folder, parent_id, content_format, size, "
|
|
||||||
"COALESCE(updated_at, created_at) as updated_at "
|
|
||||||
"FROM local_workspace_items "
|
|
||||||
"WHERE workspace_id = ? AND deleted_at IS NULL "
|
|
||||||
"ORDER BY is_folder DESC, name COLLATE NOCASE",
|
|
||||||
[ws_id],
|
|
||||||
).fetchall()
|
|
||||||
|
|
||||||
items = []
|
|
||||||
for r in rows:
|
|
||||||
name = r["name"] or "Untitled"
|
|
||||||
is_folder = bool(r["is_folder"])
|
|
||||||
icon = "📁" if is_folder else "📄"
|
|
||||||
fn = name.lower()
|
|
||||||
if not is_folder:
|
|
||||||
if fn.endswith(".pdf"):
|
|
||||||
icon = "📕"
|
|
||||||
elif any(fn.endswith(e) for e in [".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg"]):
|
|
||||||
icon = "🖼️"
|
|
||||||
elif any(fn.endswith(e) for e in [".py", ".js", ".ts", ".go", ".rs"]):
|
|
||||||
icon = "📜"
|
|
||||||
|
|
||||||
# Check for children
|
|
||||||
child_count = conn.execute(
|
|
||||||
"SELECT COUNT(*) FROM local_workspace_items WHERE parent_id=? AND deleted_at IS NULL",
|
|
||||||
[r["id"]],
|
|
||||||
).fetchone()[0]
|
|
||||||
|
|
||||||
items.append({
|
|
||||||
"id": r["id"],
|
|
||||||
"title": name,
|
|
||||||
"icon": icon,
|
|
||||||
"is_folder": is_folder,
|
|
||||||
"source_type": "local-ws",
|
|
||||||
"source_label": ws.get("name", "Workspace"),
|
|
||||||
"workspace": ws.get("name", ""),
|
|
||||||
"workspace_name": ws.get("name", ""),
|
|
||||||
"author": "",
|
|
||||||
"author_initial": "?",
|
|
||||||
"updated_at": r["updated_at"] or "",
|
|
||||||
"visited_at": "",
|
|
||||||
"has_children": child_count > 0,
|
|
||||||
"children": [],
|
|
||||||
"url": f"/local-workspace?folder={r['id']}" if is_folder else f"/pages/{r['id']}",
|
|
||||||
"content_format": r["content_format"] or "file",
|
|
||||||
"favorited": False,
|
|
||||||
"page_icon": "",
|
|
||||||
"tags": [],
|
|
||||||
"size_display": _format_size(r["size"]) if r["size"] else "",
|
|
||||||
})
|
|
||||||
|
|
||||||
return {"items": items}
|
|
||||||
|
|
||||||
|
|
||||||
def _format_size(size_bytes):
|
|
||||||
if not size_bytes:
|
|
||||||
return ""
|
|
||||||
if size_bytes < 1024:
|
|
||||||
return f"{size_bytes} B"
|
|
||||||
if size_bytes < 1048576:
|
|
||||||
return f"{size_bytes/1024:.1f} KB"
|
|
||||||
if size_bytes < 1073741824:
|
|
||||||
return f"{size_bytes/1048576:.1f} MB"
|
|
||||||
return f"{size_bytes/1073741824:.1f} GB"
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/workspace")
|
@router.get("/workspace")
|
||||||
def library_workspace(
|
def library_workspace(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
|||||||
+1
-84
@@ -2,7 +2,7 @@
|
|||||||
"openapi": "3.1.0",
|
"openapi": "3.1.0",
|
||||||
"info": {
|
"info": {
|
||||||
"title": "FlowDeck",
|
"title": "FlowDeck",
|
||||||
"version": "7.11.0"
|
"version": "7.12.0"
|
||||||
},
|
},
|
||||||
"paths": {
|
"paths": {
|
||||||
"/auth/register": {
|
"/auth/register": {
|
||||||
@@ -10765,47 +10765,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/library/local-workspace-children/{item_id}": {
|
|
||||||
"get": {
|
|
||||||
"tags": [
|
|
||||||
"library"
|
|
||||||
],
|
|
||||||
"summary": "Library Local Workspace Children",
|
|
||||||
"description": "Return children of a local workspace item for tree expansion.",
|
|
||||||
"operationId": "library_local_workspace_children_api_library_local_workspace_children__item_id__get",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "item_id",
|
|
||||||
"in": "path",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"type": "integer",
|
|
||||||
"title": "Item Id"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "Successful Response",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"422": {
|
|
||||||
"description": "Validation Error",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/HTTPValidationError"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/library/children/{page_id}": {
|
"/api/library/children/{page_id}": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
@@ -10899,48 +10858,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/library/local-workspace": {
|
|
||||||
"get": {
|
|
||||||
"tags": [
|
|
||||||
"library"
|
|
||||||
],
|
|
||||||
"summary": "Library Local Workspace",
|
|
||||||
"description": "Return local workspace items (files/folders) formatted for Library display.",
|
|
||||||
"operationId": "library_local_workspace_api_library_local_workspace_get",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "workspace_id",
|
|
||||||
"in": "query",
|
|
||||||
"required": false,
|
|
||||||
"schema": {
|
|
||||||
"type": "integer",
|
|
||||||
"default": 0,
|
|
||||||
"title": "Workspace Id"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "Successful Response",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"422": {
|
|
||||||
"description": "Validation Error",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/HTTPValidationError"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/library/workspace": {
|
"/api/library/workspace": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
|
|||||||
@@ -107,3 +107,80 @@ def test_github_disconnect_ok(client):
|
|||||||
r = client.delete("/api/github/disconnect")
|
r = client.delete("/api/github/disconnect")
|
||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
assert r.json() == {"status": "ok"}
|
assert r.json() == {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
|
# ── library.py (1/10 → 10/10) ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
def _seed_page(title: str, **cols) -> int:
|
||||||
|
"""Insert minimale (title/content/format + colonnes surnuméraires), id renvoyé."""
|
||||||
|
from app.db import get_conn
|
||||||
|
|
||||||
|
columns = ["workspace", "title", "content", "content_format", *cols]
|
||||||
|
placeholders = ", ".join("?" for _ in columns)
|
||||||
|
values = ["", title, "", "markdown", *cols.values()]
|
||||||
|
with get_conn() as conn:
|
||||||
|
cur = conn.execute(
|
||||||
|
f"INSERT INTO pages ({', '.join(columns)}) VALUES ({placeholders})", values
|
||||||
|
)
|
||||||
|
pid = cur.lastrowid
|
||||||
|
conn.commit()
|
||||||
|
return pid
|
||||||
|
|
||||||
|
|
||||||
|
def test_library_all_lists_return_items(client):
|
||||||
|
"""Les 5 listes de la bibliothèque répondent {"items": [...]}."""
|
||||||
|
for path in ("/recents", "/favorites", "/published", "/private", "/workspace"):
|
||||||
|
r = client.get(f"/api/library{path}")
|
||||||
|
assert r.status_code == 200, (path, r.status_code)
|
||||||
|
assert isinstance(r.json()["items"], list), path
|
||||||
|
|
||||||
|
|
||||||
|
def test_library_private_lists_seeded_page(client):
|
||||||
|
from app.db import get_conn
|
||||||
|
|
||||||
|
pid = _seed_page("A32 private page", parent_section="Private")
|
||||||
|
try:
|
||||||
|
items = client.get("/api/library/private").json()["items"]
|
||||||
|
assert "A32 private page" in [i["title"] for i in items]
|
||||||
|
finally:
|
||||||
|
with get_conn() as conn:
|
||||||
|
conn.execute("DELETE FROM pages WHERE id=?", (pid,))
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def test_library_children_lists_child(client):
|
||||||
|
from app.db import get_conn
|
||||||
|
|
||||||
|
parent = _seed_page("A32 parent")
|
||||||
|
child = _seed_page("A32 child", parent_id=parent)
|
||||||
|
try:
|
||||||
|
items = client.get(f"/api/library/children/{parent}").json()["items"]
|
||||||
|
assert [i["title"] for i in items] == ["A32 child"]
|
||||||
|
finally:
|
||||||
|
with get_conn() as conn:
|
||||||
|
conn.execute("DELETE FROM pages WHERE id IN (?, ?)", (parent, child))
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def test_library_local_workspace_routes_removed(client):
|
||||||
|
"""A32 → A30 : ces 2 routes lisaient `local_workspace_items`, une table
|
||||||
|
inexistante nulle part dans le codebase (500 systématique côté children,
|
||||||
|
500 d?s qu'un workspace existe côté local-workspace) et AUCUNE référence
|
||||||
|
front → supprimées avec `_format_size` devenu mort."""
|
||||||
|
assert client.get("/api/library/local-workspace-children/1").status_code == 404
|
||||||
|
assert client.get("/api/library/local-workspace").status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_library_repository_empty_and_keyed(client):
|
||||||
|
empty = client.get("/api/library/repository")
|
||||||
|
assert empty.status_code == 200 and empty.json() == {"items": []}
|
||||||
|
keyed = client.get("/api/library/repository?gitea_owner=acme&gitea_repo=flowdeck")
|
||||||
|
assert keyed.status_code == 200
|
||||||
|
assert isinstance(keyed.json()["items"], list)
|
||||||
|
|
||||||
|
|
||||||
|
def test_library_lists_still_available_after_removal(client):
|
||||||
|
"""Les listes restantes (celles qu'utilise le front) répondent toujours."""
|
||||||
|
r = client.get("/api/library/recents")
|
||||||
|
assert r.status_code == 200 and isinstance(r.json()["items"], list)
|
||||||
|
|||||||
Reference in New Issue
Block a user