feat: phases 77–80 — navbar view refresh, static background, API tokens, history suggestion chips
Single consolidated commit for four completed, validated phases (77, 78, 79, 80). The pipeline run left all work uncommitted because the harness commits only with PHASE_COMMIT=1 while child executors are forbidden from committing; the phases themselves all passed validation and moved to .agents/phases/complete/. Phase 77 — navbar view refresh - router.js dispatches bor:view-refresh on re-show / active re-click / popstate (gated on wasMounted; first show and boot exempt) - History / RAG / Sources / Tuning re-fetch on refresh (admin branch); Chat deliberately excluded (stream survival) - History "Refresh" button (admin-only, in-flight disable + status line) - New story suite tests/e2e/test_navbar_refresh.py (7 tests) Phase 78 — static background - Removed the animated glow layers; static 44px grid over the flat --bg canvas; default and reduced-motion renders byte-identical - Updated background/theme E2E suites; removed bg-glow test pins Phase 79 — API tokens - api_tokens model + migration 0012; hash-only token service - Admin tokens API + Tokens admin view; POST /api/token-auth; live-revoking require_user on chat / suggestions / document content - Frontend token gate with localStorage cache; anonymous E2E suites migrated to token login - New story suite tests/e2e/test_api_tokens.py (9 tests) Phase 80 — history suggestion chips - last_questions() endpoint with SEED fallback; startNewChat() refetch - Seed-semantics docs (config.py, .env.example, README) - Integration state matrix + E2E suite rewritten to the 4 chip states Also included: phase-76 report artifacts and the repo restore-test-db skill (previously untracked), scripts/* ruff fixes from phase 77. Final gate state (phase 80 final pass, covers everything above): - uv run pytest --cov=app → 1637 passed, 0 failed, app/ coverage 99% - uv run ruff check . && uv run pyright → clean, 0 errors - Per-phase story E2E suites green in isolation
This commit is contained in:
@@ -0,0 +1,8 @@
|
|||||||
|
**Task 06 (phase 76 final) — complete.**
|
||||||
|
|
||||||
|
- **Gates (item 1):** `uv run pytest --cov=app` → 1565 passed, app/ **99%** (>90%); `uv run ruff check . && uv run pyright` → clean; isolation spot-checks — all 8 watch-list suites **PASS** (nav_switch_keeps_stream, sources_midstream_bug, hidden_tab_stream, stop_generation, chat_persistence, chat_history, header_consistency, cache_busting).
|
||||||
|
- **Full E2E batch:** 286 passed / 23 failed in one process; all 23 re-ran **green in isolation** (11 files, +8 watch-list = 19 suites PASS) — the known pre-existing port-sharing artifact (documented in this phase's task-03 report; AGENTS.md rule 9 = isolation is the protocol). No phase-76 regressions found; no prior defects to fix.
|
||||||
|
- **Owner repro (real browser, real LLM, :8010):** `tell me about everquest` → RAG mid-stream → Chat: window sentinel survived (same document), FULL answer, **one** brain bubble, no error banner, **+1 settled `query_log` row**, zero `turn cancelled` lines; auto-saved row matches. Control (real nav to `/shared.html` mid-stream): sentinel wiped, no settled row, cancel line logged, partial persisted. 9 screenshots in `.agents/screenshots/` (tracked, committed).
|
||||||
|
- **Commit:** `ffa919b fix(chat): keep in-flight answers alive across in-app view switches` (`--no-gpg-sign`, 78 files) — body records root cause, the owner-confirmed phase-48 refinement, Option A over B/C, boundaries, and the phase-20 suite rewrite. Phase dir moved to `.agents/phases/complete/`; `git status` clean.
|
||||||
|
- **Deviations:** n/a file is empty (no further phases queued); used `/shared.html` for the control (not `/login.html`, which auto-redirects admins) — same real-departure control the suite pins; the repro's answer was an honest deflection (`deflected=True`) since the dev KB holds no EverQuest docs — the settle (not the topic) is the proof, as expected post-e2e fixture seeding.
|
||||||
|
- **Next pending task:** none — phase 76 complete; `todo/` is empty.
|
||||||
+78
@@ -0,0 +1,78 @@
|
|||||||
|
........................................................................ [ 4%]
|
||||||
|
........................................................................ [ 9%]
|
||||||
|
........................................................................ [ 13%]
|
||||||
|
........................................................................ [ 18%]
|
||||||
|
........................................................................ [ 23%]
|
||||||
|
........................................................................ [ 27%]
|
||||||
|
........................................................................ [ 32%]
|
||||||
|
........................................................................ [ 36%]
|
||||||
|
........................................................................ [ 41%]
|
||||||
|
........................................................................ [ 46%]
|
||||||
|
........................................................................ [ 50%]
|
||||||
|
........................................................................ [ 55%]
|
||||||
|
........................................................................ [ 59%]
|
||||||
|
........................................................................ [ 64%]
|
||||||
|
........................................................................ [ 69%]
|
||||||
|
........................................................................ [ 73%]
|
||||||
|
........................................................................ [ 78%]
|
||||||
|
........................................................................ [ 82%]
|
||||||
|
........................................................................ [ 87%]
|
||||||
|
........................................................................ [ 92%]
|
||||||
|
........................................................................ [ 96%]
|
||||||
|
..................................................... [100%]
|
||||||
|
=============================== warnings summary ===============================
|
||||||
|
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
|
||||||
|
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
|
||||||
|
from starlette.testclient import TestClient as TestClient # noqa
|
||||||
|
|
||||||
|
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
|
||||||
|
================================ tests coverage ================================
|
||||||
|
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
|
||||||
|
|
||||||
|
Name Stmts Miss Cover
|
||||||
|
-----------------------------------------------
|
||||||
|
app/__init__.py 1 0 100%
|
||||||
|
app/api/__init__.py 0 0 100%
|
||||||
|
app/api/auth.py 22 0 100%
|
||||||
|
app/api/chat.py 177 0 100%
|
||||||
|
app/api/chats.py 110 0 100%
|
||||||
|
app/api/config.py 7 0 100%
|
||||||
|
app/api/doc_drafts.py 93 0 100%
|
||||||
|
app/api/docs.py 50 0 100%
|
||||||
|
app/api/git_sources.py 212 0 100%
|
||||||
|
app/api/health.py 10 0 100%
|
||||||
|
app/api/steering.py 42 0 100%
|
||||||
|
app/api/suggestions.py 8 0 100%
|
||||||
|
app/api/sync.py 101 0 100%
|
||||||
|
app/config.py 140 0 100%
|
||||||
|
app/core/__init__.py 0 0 100%
|
||||||
|
app/core/auth.py 20 0 100%
|
||||||
|
app/core/caching.py 108 0 100%
|
||||||
|
app/core/debugging.py 29 2 93%
|
||||||
|
app/core/docs_push.py 39 0 100%
|
||||||
|
app/core/logging.py 13 0 100%
|
||||||
|
app/db.py 21 0 100%
|
||||||
|
app/main.py 60 0 100%
|
||||||
|
app/models.py 86 0 100%
|
||||||
|
app/rag/__init__.py 0 0 100%
|
||||||
|
app/rag/agent.py 222 0 100%
|
||||||
|
app/rag/archive_upload.py 128 0 100%
|
||||||
|
app/rag/chunker.py 206 4 98%
|
||||||
|
app/rag/git_sources.py 14 0 100%
|
||||||
|
app/rag/importer.py 180 3 98%
|
||||||
|
app/rag/llm.py 216 0 100%
|
||||||
|
app/rag/overview.py 71 0 100%
|
||||||
|
app/rag/prompts.py 88 0 100%
|
||||||
|
app/rag/retriever.py 150 3 98%
|
||||||
|
app/rag/scaffolding.py 55 0 100%
|
||||||
|
app/rag/source_removal.py 41 0 100%
|
||||||
|
app/rag/sources_meta.py 16 0 100%
|
||||||
|
app/rag/suggestions.py 27 0 100%
|
||||||
|
app/rag/summarizer.py 24 0 100%
|
||||||
|
app/schemas.py 210 0 100%
|
||||||
|
-----------------------------------------------
|
||||||
|
TOTAL 2997 12 99%
|
||||||
|
coverage gate: app/ 99% (>90%) OK
|
||||||
|
All checks passed!
|
||||||
|
0 errors, 0 warnings, 0 informations
|
||||||
|
validation OK
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
All gates green. This was the final verification pass — every task (01–03) was already implemented in the working tree and its task files were already staged in `.agents/phases/complete/77_navbar_refresh/`; I verified the implementation against the task specs, the E2E suite, and the unit pins, and ran every completion criterion. No defects found; no fixes needed.
|
||||||
|
|
||||||
|
**Phase 77 — final verification report**
|
||||||
|
|
||||||
|
- Verified (already implemented in working tree): `bor:view-refresh` dispatch in `router.js` (re-show + active re-click + popstate, `wasMounted` pre-mount gate, first-show/boot exempt); re-entrant re-fetch in History/RAG/Sources/Tuning (admin-branch listeners, Chat excluded with pinning comment); `#history-refresh` button (page-head, admin-only, in-flight disable + `#history-status` announce); unit pins in `test_frontend_router.py`; new story suite `test_navbar_refresh.py` (7 tests incl. sentinel, no-pushState, request-log, stream-survival control).
|
||||||
|
- `uv run pytest tests/e2e/test_navbar_refresh.py -v --no-cov` → **7 passed** (17.6s, isolated, DB up). ✅
|
||||||
|
- `uv run pytest tests/e2e/test_nav_switch_keeps_stream.py -v --no-cov` → **4 passed**; file byte-identical (no git diff). ✅
|
||||||
|
- `uv run pytest --cov=app --cov-report=term-missing` → **1575 passed**, TOTAL coverage **99%** (>90% floor). ✅
|
||||||
|
- `uv run ruff check . && uv run pyright` → clean (0 errors). ✅
|
||||||
|
- Commit/phase-dir move: left to the harness per executor rules (task files already in `complete/`; no `git add`/`commit` run by me).
|
||||||
|
- Notable: pre-existing uncommitted changes to `scripts/*` and phase-76 report artifacts are in the tree from earlier work — untouched, will ride along in the harness commit.
|
||||||
|
- Next pending phase: `78_static_background`.
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
........................................................................ [ 4%]
|
||||||
|
........................................................................ [ 9%]
|
||||||
|
........................................................................ [ 13%]
|
||||||
|
........................................................................ [ 18%]
|
||||||
|
........................................................................ [ 22%]
|
||||||
|
........................................................................ [ 27%]
|
||||||
|
........................................................................ [ 32%]
|
||||||
|
........................................................................ [ 36%]
|
||||||
|
........................................................................ [ 41%]
|
||||||
|
........................................................................ [ 45%]
|
||||||
|
........................................................................ [ 50%]
|
||||||
|
........................................................................ [ 54%]
|
||||||
|
........................................................................ [ 59%]
|
||||||
|
........................................................................ [ 64%]
|
||||||
|
........................................................................ [ 68%]
|
||||||
|
........................................................................ [ 73%]
|
||||||
|
........................................................................ [ 77%]
|
||||||
|
........................................................................ [ 82%]
|
||||||
|
........................................................................ [ 86%]
|
||||||
|
........................................................................ [ 91%]
|
||||||
|
........................................................................ [ 96%]
|
||||||
|
............................................................... [100%]
|
||||||
|
=============================== warnings summary ===============================
|
||||||
|
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
|
||||||
|
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
|
||||||
|
from starlette.testclient import TestClient as TestClient # noqa
|
||||||
|
|
||||||
|
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
|
||||||
|
================================ tests coverage ================================
|
||||||
|
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
|
||||||
|
|
||||||
|
Name Stmts Miss Cover
|
||||||
|
-----------------------------------------------
|
||||||
|
app/__init__.py 1 0 100%
|
||||||
|
app/api/__init__.py 0 0 100%
|
||||||
|
app/api/auth.py 22 0 100%
|
||||||
|
app/api/chat.py 177 0 100%
|
||||||
|
app/api/chats.py 110 0 100%
|
||||||
|
app/api/config.py 7 0 100%
|
||||||
|
app/api/doc_drafts.py 93 0 100%
|
||||||
|
app/api/docs.py 50 0 100%
|
||||||
|
app/api/git_sources.py 212 0 100%
|
||||||
|
app/api/health.py 10 0 100%
|
||||||
|
app/api/steering.py 42 0 100%
|
||||||
|
app/api/suggestions.py 8 0 100%
|
||||||
|
app/api/sync.py 101 0 100%
|
||||||
|
app/config.py 140 0 100%
|
||||||
|
app/core/__init__.py 0 0 100%
|
||||||
|
app/core/auth.py 20 0 100%
|
||||||
|
app/core/caching.py 108 0 100%
|
||||||
|
app/core/debugging.py 29 2 93%
|
||||||
|
app/core/docs_push.py 39 0 100%
|
||||||
|
app/core/logging.py 13 0 100%
|
||||||
|
app/db.py 21 0 100%
|
||||||
|
app/main.py 60 0 100%
|
||||||
|
app/models.py 86 0 100%
|
||||||
|
app/rag/__init__.py 0 0 100%
|
||||||
|
app/rag/agent.py 222 0 100%
|
||||||
|
app/rag/archive_upload.py 128 0 100%
|
||||||
|
app/rag/chunker.py 206 4 98%
|
||||||
|
app/rag/git_sources.py 14 0 100%
|
||||||
|
app/rag/importer.py 180 3 98%
|
||||||
|
app/rag/llm.py 216 0 100%
|
||||||
|
app/rag/overview.py 71 0 100%
|
||||||
|
app/rag/prompts.py 88 0 100%
|
||||||
|
app/rag/retriever.py 150 3 98%
|
||||||
|
app/rag/scaffolding.py 55 0 100%
|
||||||
|
app/rag/source_removal.py 41 0 100%
|
||||||
|
app/rag/sources_meta.py 16 0 100%
|
||||||
|
app/rag/suggestions.py 27 0 100%
|
||||||
|
app/rag/summarizer.py 24 0 100%
|
||||||
|
app/schemas.py 210 0 100%
|
||||||
|
-----------------------------------------------
|
||||||
|
TOTAL 2997 12 99%
|
||||||
|
coverage gate: app/ 99% (>90%) OK
|
||||||
|
All checks passed!
|
||||||
|
0 errors, 0 warnings, 0 informations
|
||||||
|
validation OK
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
Task 01 complete. All gates green.
|
||||||
|
|
||||||
|
**Implemented**
|
||||||
|
- `frontend/assets/router.js`: `switchTo` captures `const wasMounted = mounted[name]` before the mount block and dispatches `bor:view-refresh` on the view's section after show + head/nav state (before the focus tail), gated on `wasMounted` — first show and boot never fire it; popstate inherits the gate. The `name === current` re-click branch now dispatches the event on `viewEls[name]` and returns (no `pushState`, menu-close unchanged). Header contract comment updated.
|
||||||
|
- `frontend/assets/history.js`: `loadChats()` is re-entrant (drops every `tr` except `#history-empty-row` before fetching — replaces, never duplicates); the ADMIN branch (after the whoami gate) arms `root.addEventListener("bor:view-refresh", () => { if (started) loadChats(); })` with `started` flipping true at the first `loadChats()`; anonymous branch never arms a listener.
|
||||||
|
- `tests/unit/test_frontend_router.py`: 3 new source pins — event literal + dispatch inside the pre-mount `wasMounted` capture with visible→refresh→focus ordering; re-click branch dispatches (no pushState/switchTo); history listener admin-branch-only + gated + row-clearing precedes fetch.
|
||||||
|
- Fixed 9 pre-existing ruff errors in `scripts/` (left by commit f221b10) so the gate is green.
|
||||||
|
|
||||||
|
**Results**
|
||||||
|
- `uv run pytest tests/unit/test_frontend_router.py -v` → 16 passed
|
||||||
|
- `uv run pytest` → 1568 passed
|
||||||
|
- `uv run pytest --cov=app --cov-report=term-missing` → TOTAL 99% (>90% floor)
|
||||||
|
- `uv run ruff check . && uv run pyright` → clean
|
||||||
|
- `uv run pytest tests/e2e/test_nav_switch_keeps_stream.py -v --no-cov` → 4 passed (phase-76 stream-survival contract holds with the hook live, runs the new router code in a real browser)
|
||||||
|
- `node --check` on both edited JS files → OK
|
||||||
|
|
||||||
|
**Decisions**: followed the phase design exactly (gate on `wasMounted` only — boot-onto-chat dispatch is harmless since Chat never listens); optional manual spot-check covered by the phase-76 E2E + syntax checks (the dedicated freshness E2E lands in task 03). No deviations.
|
||||||
|
|
||||||
|
**Next pending task**: `.agents/phases/todo/77_navbar_refresh/02_refresh_other_views.md`
|
||||||
+78
@@ -0,0 +1,78 @@
|
|||||||
|
........................................................................ [ 4%]
|
||||||
|
........................................................................ [ 9%]
|
||||||
|
........................................................................ [ 13%]
|
||||||
|
........................................................................ [ 18%]
|
||||||
|
........................................................................ [ 22%]
|
||||||
|
........................................................................ [ 27%]
|
||||||
|
........................................................................ [ 32%]
|
||||||
|
........................................................................ [ 36%]
|
||||||
|
........................................................................ [ 41%]
|
||||||
|
........................................................................ [ 45%]
|
||||||
|
........................................................................ [ 50%]
|
||||||
|
........................................................................ [ 55%]
|
||||||
|
........................................................................ [ 59%]
|
||||||
|
........................................................................ [ 64%]
|
||||||
|
........................................................................ [ 68%]
|
||||||
|
........................................................................ [ 73%]
|
||||||
|
........................................................................ [ 78%]
|
||||||
|
........................................................................ [ 82%]
|
||||||
|
........................................................................ [ 87%]
|
||||||
|
........................................................................ [ 91%]
|
||||||
|
........................................................................ [ 96%]
|
||||||
|
........................................................ [100%]
|
||||||
|
=============================== warnings summary ===============================
|
||||||
|
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
|
||||||
|
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
|
||||||
|
from starlette.testclient import TestClient as TestClient # noqa
|
||||||
|
|
||||||
|
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
|
||||||
|
================================ tests coverage ================================
|
||||||
|
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
|
||||||
|
|
||||||
|
Name Stmts Miss Cover
|
||||||
|
-----------------------------------------------
|
||||||
|
app/__init__.py 1 0 100%
|
||||||
|
app/api/__init__.py 0 0 100%
|
||||||
|
app/api/auth.py 22 0 100%
|
||||||
|
app/api/chat.py 177 0 100%
|
||||||
|
app/api/chats.py 110 0 100%
|
||||||
|
app/api/config.py 7 0 100%
|
||||||
|
app/api/doc_drafts.py 93 0 100%
|
||||||
|
app/api/docs.py 50 0 100%
|
||||||
|
app/api/git_sources.py 212 0 100%
|
||||||
|
app/api/health.py 10 0 100%
|
||||||
|
app/api/steering.py 42 0 100%
|
||||||
|
app/api/suggestions.py 8 0 100%
|
||||||
|
app/api/sync.py 101 0 100%
|
||||||
|
app/config.py 140 0 100%
|
||||||
|
app/core/__init__.py 0 0 100%
|
||||||
|
app/core/auth.py 20 0 100%
|
||||||
|
app/core/caching.py 108 0 100%
|
||||||
|
app/core/debugging.py 29 2 93%
|
||||||
|
app/core/docs_push.py 39 0 100%
|
||||||
|
app/core/logging.py 13 0 100%
|
||||||
|
app/db.py 21 0 100%
|
||||||
|
app/main.py 60 0 100%
|
||||||
|
app/models.py 86 0 100%
|
||||||
|
app/rag/__init__.py 0 0 100%
|
||||||
|
app/rag/agent.py 222 0 100%
|
||||||
|
app/rag/archive_upload.py 128 0 100%
|
||||||
|
app/rag/chunker.py 206 4 98%
|
||||||
|
app/rag/git_sources.py 14 0 100%
|
||||||
|
app/rag/importer.py 180 3 98%
|
||||||
|
app/rag/llm.py 216 0 100%
|
||||||
|
app/rag/overview.py 71 0 100%
|
||||||
|
app/rag/prompts.py 88 0 100%
|
||||||
|
app/rag/retriever.py 150 3 98%
|
||||||
|
app/rag/scaffolding.py 55 0 100%
|
||||||
|
app/rag/source_removal.py 41 0 100%
|
||||||
|
app/rag/sources_meta.py 16 0 100%
|
||||||
|
app/rag/suggestions.py 27 0 100%
|
||||||
|
app/rag/summarizer.py 24 0 100%
|
||||||
|
app/schemas.py 210 0 100%
|
||||||
|
-----------------------------------------------
|
||||||
|
TOTAL 2997 12 99%
|
||||||
|
coverage gate: app/ 99% (>90%) OK
|
||||||
|
All checks passed!
|
||||||
|
0 errors, 0 warnings, 0 informations
|
||||||
|
validation OK
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
All green. Final report:
|
||||||
|
|
||||||
|
**Implemented (task 02 — RAG/Sources/Tuning join the refresh, Chat stays out):**
|
||||||
|
- `sources.js`: `loadDocs()` re-entrant — `tbody.replaceChildren()` moved to the top (before fetch; removed the redundant populated-path clear) so populated→empty refreshes leave no ghost rows; `bor:view-refresh` listener armed in the admin branch after the whoami gate
|
||||||
|
- `git-sources.js`: listener `() => loadSources()` in admin branch; verified a re-call resets all three states (success path runs `hideLoadError()` before `renderSources`, which replaces the tbody)
|
||||||
|
- `tuning.js`: listener `() => loadNotes()` in admin branch; failed-refresh keeps last rendered list (documented contract, unchanged)
|
||||||
|
- `app.js`: NO listener — one-line comment at the module-scope state pinning the deliberate exclusion (phase-76 stream survival)
|
||||||
|
- `tests/unit/test_frontend_router.py`: 4 new pins — per-view listener + gate ordering + re-entrancy checks, plus the app.js negative pin (targets the listener, not the literal, since the exclusion comment carries the literal)
|
||||||
|
|
||||||
|
**Results:**
|
||||||
|
- `uv run pytest tests/unit/test_frontend_router.py -v --no-cov` → 20 passed
|
||||||
|
- `uv run pytest --cov=app --cov-report=term-missing` → 1572 passed; `app/` coverage 99% (>90%)
|
||||||
|
- `uv run ruff check . && uv run pyright` → clean (0 errors)
|
||||||
|
- `uv run pytest tests/e2e/test_nav_switch_keeps_stream.py -v --no-cov` → 4 passed (unchanged, stream-survival holds)
|
||||||
|
|
||||||
|
**Decisions:** git-sources listener placed before `await loadSources()` to preserve the existing "LAST statement" unit pin; no `started` flag needed (listener armed post-gate, per task text).
|
||||||
|
|
||||||
|
**Next pending task:** `77_navbar_refresh/03_history_refresh_button.md` (History Refresh button + story E2E suite + regression sweep).
|
||||||
+78
@@ -0,0 +1,78 @@
|
|||||||
|
........................................................................ [ 4%]
|
||||||
|
........................................................................ [ 9%]
|
||||||
|
........................................................................ [ 13%]
|
||||||
|
........................................................................ [ 18%]
|
||||||
|
........................................................................ [ 22%]
|
||||||
|
........................................................................ [ 27%]
|
||||||
|
........................................................................ [ 32%]
|
||||||
|
........................................................................ [ 36%]
|
||||||
|
........................................................................ [ 41%]
|
||||||
|
........................................................................ [ 45%]
|
||||||
|
........................................................................ [ 50%]
|
||||||
|
........................................................................ [ 54%]
|
||||||
|
........................................................................ [ 59%]
|
||||||
|
........................................................................ [ 64%]
|
||||||
|
........................................................................ [ 68%]
|
||||||
|
........................................................................ [ 73%]
|
||||||
|
........................................................................ [ 77%]
|
||||||
|
........................................................................ [ 82%]
|
||||||
|
........................................................................ [ 87%]
|
||||||
|
........................................................................ [ 91%]
|
||||||
|
........................................................................ [ 96%]
|
||||||
|
............................................................ [100%]
|
||||||
|
=============================== warnings summary ===============================
|
||||||
|
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
|
||||||
|
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
|
||||||
|
from starlette.testclient import TestClient as TestClient # noqa
|
||||||
|
|
||||||
|
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
|
||||||
|
================================ tests coverage ================================
|
||||||
|
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
|
||||||
|
|
||||||
|
Name Stmts Miss Cover
|
||||||
|
-----------------------------------------------
|
||||||
|
app/__init__.py 1 0 100%
|
||||||
|
app/api/__init__.py 0 0 100%
|
||||||
|
app/api/auth.py 22 0 100%
|
||||||
|
app/api/chat.py 177 0 100%
|
||||||
|
app/api/chats.py 110 0 100%
|
||||||
|
app/api/config.py 7 0 100%
|
||||||
|
app/api/doc_drafts.py 93 0 100%
|
||||||
|
app/api/docs.py 50 0 100%
|
||||||
|
app/api/git_sources.py 212 0 100%
|
||||||
|
app/api/health.py 10 0 100%
|
||||||
|
app/api/steering.py 42 0 100%
|
||||||
|
app/api/suggestions.py 8 0 100%
|
||||||
|
app/api/sync.py 101 0 100%
|
||||||
|
app/config.py 140 0 100%
|
||||||
|
app/core/__init__.py 0 0 100%
|
||||||
|
app/core/auth.py 20 0 100%
|
||||||
|
app/core/caching.py 108 0 100%
|
||||||
|
app/core/debugging.py 29 2 93%
|
||||||
|
app/core/docs_push.py 39 0 100%
|
||||||
|
app/core/logging.py 13 0 100%
|
||||||
|
app/db.py 21 0 100%
|
||||||
|
app/main.py 60 0 100%
|
||||||
|
app/models.py 86 0 100%
|
||||||
|
app/rag/__init__.py 0 0 100%
|
||||||
|
app/rag/agent.py 222 0 100%
|
||||||
|
app/rag/archive_upload.py 128 0 100%
|
||||||
|
app/rag/chunker.py 206 4 98%
|
||||||
|
app/rag/git_sources.py 14 0 100%
|
||||||
|
app/rag/importer.py 180 3 98%
|
||||||
|
app/rag/llm.py 216 0 100%
|
||||||
|
app/rag/overview.py 71 0 100%
|
||||||
|
app/rag/prompts.py 88 0 100%
|
||||||
|
app/rag/retriever.py 150 3 98%
|
||||||
|
app/rag/scaffolding.py 55 0 100%
|
||||||
|
app/rag/source_removal.py 41 0 100%
|
||||||
|
app/rag/sources_meta.py 16 0 100%
|
||||||
|
app/rag/suggestions.py 27 0 100%
|
||||||
|
app/rag/summarizer.py 24 0 100%
|
||||||
|
app/schemas.py 210 0 100%
|
||||||
|
-----------------------------------------------
|
||||||
|
TOTAL 2997 12 99%
|
||||||
|
coverage gate: app/ 99% (>90%) OK
|
||||||
|
All checks passed!
|
||||||
|
0 errors, 0 warnings, 0 informations
|
||||||
|
validation OK
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
Everything is complete and verified. Final state of all gates (run in the final file state):
|
||||||
|
|
||||||
|
**Implemented (task 03):**
|
||||||
|
- `frontend/index.html` — History `.page-head` is now a flex row (title block left, actions right): `#history-refresh` button with the house inline-SVG refresh glyph (`aria-hidden`) + visible "Refresh" label, `aria-label="Refresh saved chats"`, outside the table wrap
|
||||||
|
- `frontend/assets/history.js` — admin-branch binding: click → disable → `loadChats()` → announce `Saved chats refreshed.` (0-row = success) / failure lines (`Couldn't load saved chats — is the app reachable?` / `— try again.` now live inside `loadChats`, so every caller sees them) → re-enable in `finally`; anonymous branch hides the button (no dead control)
|
||||||
|
- `frontend/assets/styles.css` — `.history-refresh` reuses the `.new-chat-btn` language (brand pill 5.2:1, ≥44px, hover, `:disabled`, global focus-visible); `#view-history .page-head` flex row scoped (other four views untouched); icon-only ≤640px (phase-46 convention)
|
||||||
|
- `tests/unit/test_frontend_router.py` — 3 new source pins (markup placement, admin-only binding + outcome lines, CSS language/mobile)
|
||||||
|
- `tests/e2e/test_navbar_refresh.py` — new story suite, 7 tests: re-show re-fetch (+ no-duplicates + same-doc sentinel), active re-click (no pushState, `history.length` unchanged), refresh button (disabled in-flight via the house request-hold pattern, status line, re-enable, keyboard focus), popstate re-fetch, four-views request-log (+History row-delta), stream-survival control (full mock answer + settled `query_log`), API row-shape sanity
|
||||||
|
|
||||||
|
**Test / lint / coverage results:**
|
||||||
|
- `uv run pytest tests/e2e/test_navbar_refresh.py -v --no-cov` → 7 passed (isolated, DB up)
|
||||||
|
- `uv run pytest tests/e2e/test_nav_switch_keeps_stream.py -v --no-cov` → 4 passed, unchanged
|
||||||
|
- `uv run pytest --cov=app --cov-report=term-missing` → 1575 passed, TOTAL 99% (>90% floor)
|
||||||
|
- `uv run ruff check . && uv run pyright` → clean
|
||||||
|
- Regression sweep green: `test_chat_history`, `test_share_chat`, `test_history_copy` (10), `test_stale_saved_chats`+`test_llm_history` (6), header/nav/cache/persistence (23), `test_hidden_tab_stream`+`test_stop_generation` (7)
|
||||||
|
|
||||||
|
**Decisions/deviations:** failure announce moved into `loadChats` (no existing load-failure copy existed — house "reachable?/try again." family reused, and the §7.4 contract improved for all callers); anonymous hides the button (task's "admin branch only" read as gate-gated control, not a dead button); E2E request-log polling must use `page.wait_for_timeout` (sync API starves `page.on` events under raw `time.sleep` — documented in `_wait_until`); per harness rules the atomic commit + phase-dir move are left to the harness (task's commit item intentionally not executed).
|
||||||
|
|
||||||
|
**Next pending task:** none in phase 77 (task 03 was the last); next phase in `todo/` is `78_static_background`.
|
||||||
+78
@@ -0,0 +1,78 @@
|
|||||||
|
........................................................................ [ 4%]
|
||||||
|
........................................................................ [ 9%]
|
||||||
|
........................................................................ [ 13%]
|
||||||
|
........................................................................ [ 18%]
|
||||||
|
........................................................................ [ 22%]
|
||||||
|
........................................................................ [ 27%]
|
||||||
|
........................................................................ [ 32%]
|
||||||
|
........................................................................ [ 36%]
|
||||||
|
........................................................................ [ 41%]
|
||||||
|
........................................................................ [ 45%]
|
||||||
|
........................................................................ [ 50%]
|
||||||
|
........................................................................ [ 54%]
|
||||||
|
........................................................................ [ 59%]
|
||||||
|
........................................................................ [ 64%]
|
||||||
|
........................................................................ [ 68%]
|
||||||
|
........................................................................ [ 73%]
|
||||||
|
........................................................................ [ 77%]
|
||||||
|
........................................................................ [ 82%]
|
||||||
|
........................................................................ [ 86%]
|
||||||
|
........................................................................ [ 91%]
|
||||||
|
........................................................................ [ 96%]
|
||||||
|
............................................................... [100%]
|
||||||
|
=============================== warnings summary ===============================
|
||||||
|
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
|
||||||
|
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
|
||||||
|
from starlette.testclient import TestClient as TestClient # noqa
|
||||||
|
|
||||||
|
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
|
||||||
|
================================ tests coverage ================================
|
||||||
|
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
|
||||||
|
|
||||||
|
Name Stmts Miss Cover
|
||||||
|
-----------------------------------------------
|
||||||
|
app/__init__.py 1 0 100%
|
||||||
|
app/api/__init__.py 0 0 100%
|
||||||
|
app/api/auth.py 22 0 100%
|
||||||
|
app/api/chat.py 177 0 100%
|
||||||
|
app/api/chats.py 110 0 100%
|
||||||
|
app/api/config.py 7 0 100%
|
||||||
|
app/api/doc_drafts.py 93 0 100%
|
||||||
|
app/api/docs.py 50 0 100%
|
||||||
|
app/api/git_sources.py 212 0 100%
|
||||||
|
app/api/health.py 10 0 100%
|
||||||
|
app/api/steering.py 42 0 100%
|
||||||
|
app/api/suggestions.py 8 0 100%
|
||||||
|
app/api/sync.py 101 0 100%
|
||||||
|
app/config.py 140 0 100%
|
||||||
|
app/core/__init__.py 0 0 100%
|
||||||
|
app/core/auth.py 20 0 100%
|
||||||
|
app/core/caching.py 108 0 100%
|
||||||
|
app/core/debugging.py 29 2 93%
|
||||||
|
app/core/docs_push.py 39 0 100%
|
||||||
|
app/core/logging.py 13 0 100%
|
||||||
|
app/db.py 21 0 100%
|
||||||
|
app/main.py 60 0 100%
|
||||||
|
app/models.py 86 0 100%
|
||||||
|
app/rag/__init__.py 0 0 100%
|
||||||
|
app/rag/agent.py 222 0 100%
|
||||||
|
app/rag/archive_upload.py 128 0 100%
|
||||||
|
app/rag/chunker.py 206 4 98%
|
||||||
|
app/rag/git_sources.py 14 0 100%
|
||||||
|
app/rag/importer.py 180 3 98%
|
||||||
|
app/rag/llm.py 216 0 100%
|
||||||
|
app/rag/overview.py 71 0 100%
|
||||||
|
app/rag/prompts.py 88 0 100%
|
||||||
|
app/rag/retriever.py 150 3 98%
|
||||||
|
app/rag/scaffolding.py 55 0 100%
|
||||||
|
app/rag/source_removal.py 41 0 100%
|
||||||
|
app/rag/sources_meta.py 16 0 100%
|
||||||
|
app/rag/suggestions.py 27 0 100%
|
||||||
|
app/rag/summarizer.py 24 0 100%
|
||||||
|
app/schemas.py 210 0 100%
|
||||||
|
-----------------------------------------------
|
||||||
|
TOTAL 2997 12 99%
|
||||||
|
coverage gate: app/ 99% (>90%) OK
|
||||||
|
All checks passed!
|
||||||
|
0 errors, 0 warnings, 0 informations
|
||||||
|
validation OK
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
All checks green. Final report:
|
||||||
|
|
||||||
|
**Phase 78 — Static background: verification pass complete (both tasks were already implemented; verified end-to-end, no defects found).**
|
||||||
|
|
||||||
|
- Verified: glow rules (`body::after`, `html::before`, `html::after`), `@keyframes bg-glow-a/b/c`, and the background-only `prefers-reduced-motion` block deleted from `styles.css`; `body::before` grid rule byte-identical to HEAD (44px, 60% `--line` alpha, radial mask); phase-78 comment header in place; other 7 reduced-motion blocks + UI animations untouched; `frontend/assets/themes/` (indigo) glow-free.
|
||||||
|
- Verified: `tests/unit/test_background_no_motion.py` + `tests/e2e/test_background_no_motion.py` rewritten to the static contract; both `test_background_animation.py` suites deleted; no stale references.
|
||||||
|
- Tests: `uv run pytest` → 1561 passed · `uv run pytest --cov=app --cov-report=term-missing` → 99% on `app/` (>90%) · `uv run ruff check . && uv run pyright` → clean (0 errors)
|
||||||
|
- E2E in isolation (DB up): `test_background_no_motion.py` 5 passed · `test_dark_tech_theme.py` 6 passed · `test_smoke.py` 3 passed
|
||||||
|
- Visual check (real server :8791, Chromium): static grid over flat `--bg` canvas, zero glow spots, `document.getAnimations()` empty, reduced-motion profile identical (screenshots confirmed).
|
||||||
|
- Completion criteria: 1 no bg-glow/animation in CSS + grid renders — PASS · 2 suites green, deletions gone — PASS · 3 pytest/coverage/lint — PASS · 4 commit + phase-dir move — deferred to harness per executor rules (no git commands run).
|
||||||
|
- Deviation: `test_dark_tech_theme.py` had its 2 background tests + docstring re-pinned to the static contract (its glow-fade assertions could not survive the deletion); palette/emoji/behavior tests untouched — same house pattern as phase 76.
|
||||||
|
- Next pending phase: `79_api_tokens`.
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
........................................................................ [ 4%]
|
||||||
|
........................................................................ [ 9%]
|
||||||
|
........................................................................ [ 13%]
|
||||||
|
........................................................................ [ 18%]
|
||||||
|
........................................................................ [ 23%]
|
||||||
|
........................................................................ [ 27%]
|
||||||
|
........................................................................ [ 32%]
|
||||||
|
........................................................................ [ 36%]
|
||||||
|
........................................................................ [ 41%]
|
||||||
|
........................................................................ [ 46%]
|
||||||
|
........................................................................ [ 50%]
|
||||||
|
........................................................................ [ 55%]
|
||||||
|
........................................................................ [ 59%]
|
||||||
|
........................................................................ [ 64%]
|
||||||
|
........................................................................ [ 69%]
|
||||||
|
........................................................................ [ 73%]
|
||||||
|
........................................................................ [ 78%]
|
||||||
|
........................................................................ [ 83%]
|
||||||
|
........................................................................ [ 87%]
|
||||||
|
........................................................................ [ 92%]
|
||||||
|
........................................................................ [ 96%]
|
||||||
|
................................................. [100%]
|
||||||
|
=============================== warnings summary ===============================
|
||||||
|
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
|
||||||
|
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
|
||||||
|
from starlette.testclient import TestClient as TestClient # noqa
|
||||||
|
|
||||||
|
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
|
||||||
|
================================ tests coverage ================================
|
||||||
|
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
|
||||||
|
|
||||||
|
Name Stmts Miss Cover
|
||||||
|
-----------------------------------------------
|
||||||
|
app/__init__.py 1 0 100%
|
||||||
|
app/api/__init__.py 0 0 100%
|
||||||
|
app/api/auth.py 22 0 100%
|
||||||
|
app/api/chat.py 177 0 100%
|
||||||
|
app/api/chats.py 110 0 100%
|
||||||
|
app/api/config.py 7 0 100%
|
||||||
|
app/api/doc_drafts.py 93 0 100%
|
||||||
|
app/api/docs.py 50 0 100%
|
||||||
|
app/api/git_sources.py 212 0 100%
|
||||||
|
app/api/health.py 10 0 100%
|
||||||
|
app/api/steering.py 42 0 100%
|
||||||
|
app/api/suggestions.py 8 0 100%
|
||||||
|
app/api/sync.py 101 0 100%
|
||||||
|
app/config.py 140 0 100%
|
||||||
|
app/core/__init__.py 0 0 100%
|
||||||
|
app/core/auth.py 20 0 100%
|
||||||
|
app/core/caching.py 108 0 100%
|
||||||
|
app/core/debugging.py 29 2 93%
|
||||||
|
app/core/docs_push.py 39 0 100%
|
||||||
|
app/core/logging.py 13 0 100%
|
||||||
|
app/db.py 21 0 100%
|
||||||
|
app/main.py 60 0 100%
|
||||||
|
app/models.py 86 0 100%
|
||||||
|
app/rag/__init__.py 0 0 100%
|
||||||
|
app/rag/agent.py 222 0 100%
|
||||||
|
app/rag/archive_upload.py 128 0 100%
|
||||||
|
app/rag/chunker.py 206 4 98%
|
||||||
|
app/rag/git_sources.py 14 0 100%
|
||||||
|
app/rag/importer.py 180 3 98%
|
||||||
|
app/rag/llm.py 216 0 100%
|
||||||
|
app/rag/overview.py 71 0 100%
|
||||||
|
app/rag/prompts.py 88 0 100%
|
||||||
|
app/rag/retriever.py 150 3 98%
|
||||||
|
app/rag/scaffolding.py 55 0 100%
|
||||||
|
app/rag/source_removal.py 41 0 100%
|
||||||
|
app/rag/sources_meta.py 16 0 100%
|
||||||
|
app/rag/suggestions.py 27 0 100%
|
||||||
|
app/rag/summarizer.py 24 0 100%
|
||||||
|
app/schemas.py 210 0 100%
|
||||||
|
-----------------------------------------------
|
||||||
|
TOTAL 2997 12 99%
|
||||||
|
coverage gate: app/ 99% (>90%) OK
|
||||||
|
All checks passed!
|
||||||
|
0 errors, 0 warnings, 0 informations
|
||||||
|
validation OK
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
All green. The remaining diff entries (`app.js`, `index.html`, `test_navbar_refresh.py`, etc.) are phase-77's uncommitted working-tree changes, not mine.
|
||||||
|
|
||||||
|
**Report — phase 78, task 01 (remove glow layers)**
|
||||||
|
|
||||||
|
- `styles.css`: deleted `body::after`/`html::before`/`html::after` glow rules, the three `bg-glow-*` keyframes, and the background-only reduced-motion block (7 unrelated reduced-motion blocks untouched); replaced the phase-08/25 header with a 4-line phase-78 note; `body::before` grid rule byte-identical.
|
||||||
|
- Rewrote `tests/unit/test_background_no_motion.py` (8 source pins: no `bg-*` token in CSS, glow rules absent, grid static + 44px + mask, no-occlusion plumbing, reduced-motion block gone, no blur) and `tests/e2e/test_background_no_motion.py` (5 Playwright computed-style tests incl. "no box" proof via `content: none`/`position: static`).
|
||||||
|
- Deleted `tests/unit/test_background_animation.py` + `tests/e2e/test_background_animation.py` (superseded chain 08→25→78).
|
||||||
|
- **Deviation:** `test_dark_tech_theme.py` could not stay "UNCHANGED" — it pinned `bg-glow-a/b/c` fades and glow images in `test_animated_background`/`test_reduced_motion_honored`, which the deletion breaks (and the literals violate the zero-hit rg criterion). Minimal contract-preserving update: renamed to `test_static_background` pinning the static contract, updated the reduced-motion test, dropped the now-unused `_seconds`/`pytest` import. All other tests in the file untouched.
|
||||||
|
- Tests: unit pins + themes 14 passed; e2e `test_background_no_motion.py` 5 passed in isolation; `test_dark_tech_theme.py` 6 passed; full `uv run pytest` 1561 passed; `--cov=app` **99%** (>90% floor); `uv run ruff check .` clean; `uv run pyright` 0 errors; `rg "bg-glow" app/ frontend/ tests/` zero hits; themes carry no glow/keyframes.
|
||||||
|
- Next pending task: `.agents/phases/todo/78_static_background/02_regression_sweep_commit.md`.
|
||||||
+78
@@ -0,0 +1,78 @@
|
|||||||
|
........................................................................ [ 4%]
|
||||||
|
........................................................................ [ 9%]
|
||||||
|
........................................................................ [ 13%]
|
||||||
|
........................................................................ [ 18%]
|
||||||
|
........................................................................ [ 23%]
|
||||||
|
........................................................................ [ 27%]
|
||||||
|
........................................................................ [ 32%]
|
||||||
|
........................................................................ [ 36%]
|
||||||
|
........................................................................ [ 41%]
|
||||||
|
........................................................................ [ 46%]
|
||||||
|
........................................................................ [ 50%]
|
||||||
|
........................................................................ [ 55%]
|
||||||
|
........................................................................ [ 59%]
|
||||||
|
........................................................................ [ 64%]
|
||||||
|
........................................................................ [ 69%]
|
||||||
|
........................................................................ [ 73%]
|
||||||
|
........................................................................ [ 78%]
|
||||||
|
........................................................................ [ 83%]
|
||||||
|
........................................................................ [ 87%]
|
||||||
|
........................................................................ [ 92%]
|
||||||
|
........................................................................ [ 96%]
|
||||||
|
................................................. [100%]
|
||||||
|
=============================== warnings summary ===============================
|
||||||
|
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
|
||||||
|
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
|
||||||
|
from starlette.testclient import TestClient as TestClient # noqa
|
||||||
|
|
||||||
|
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
|
||||||
|
================================ tests coverage ================================
|
||||||
|
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
|
||||||
|
|
||||||
|
Name Stmts Miss Cover
|
||||||
|
-----------------------------------------------
|
||||||
|
app/__init__.py 1 0 100%
|
||||||
|
app/api/__init__.py 0 0 100%
|
||||||
|
app/api/auth.py 22 0 100%
|
||||||
|
app/api/chat.py 177 0 100%
|
||||||
|
app/api/chats.py 110 0 100%
|
||||||
|
app/api/config.py 7 0 100%
|
||||||
|
app/api/doc_drafts.py 93 0 100%
|
||||||
|
app/api/docs.py 50 0 100%
|
||||||
|
app/api/git_sources.py 212 0 100%
|
||||||
|
app/api/health.py 10 0 100%
|
||||||
|
app/api/steering.py 42 0 100%
|
||||||
|
app/api/suggestions.py 8 0 100%
|
||||||
|
app/api/sync.py 101 0 100%
|
||||||
|
app/config.py 140 0 100%
|
||||||
|
app/core/__init__.py 0 0 100%
|
||||||
|
app/core/auth.py 20 0 100%
|
||||||
|
app/core/caching.py 108 0 100%
|
||||||
|
app/core/debugging.py 29 2 93%
|
||||||
|
app/core/docs_push.py 39 0 100%
|
||||||
|
app/core/logging.py 13 0 100%
|
||||||
|
app/db.py 21 0 100%
|
||||||
|
app/main.py 60 0 100%
|
||||||
|
app/models.py 86 0 100%
|
||||||
|
app/rag/__init__.py 0 0 100%
|
||||||
|
app/rag/agent.py 222 0 100%
|
||||||
|
app/rag/archive_upload.py 128 0 100%
|
||||||
|
app/rag/chunker.py 206 4 98%
|
||||||
|
app/rag/git_sources.py 14 0 100%
|
||||||
|
app/rag/importer.py 180 3 98%
|
||||||
|
app/rag/llm.py 216 0 100%
|
||||||
|
app/rag/overview.py 71 0 100%
|
||||||
|
app/rag/prompts.py 88 0 100%
|
||||||
|
app/rag/retriever.py 150 3 98%
|
||||||
|
app/rag/scaffolding.py 55 0 100%
|
||||||
|
app/rag/source_removal.py 41 0 100%
|
||||||
|
app/rag/sources_meta.py 16 0 100%
|
||||||
|
app/rag/suggestions.py 27 0 100%
|
||||||
|
app/rag/summarizer.py 24 0 100%
|
||||||
|
app/schemas.py 210 0 100%
|
||||||
|
-----------------------------------------------
|
||||||
|
TOTAL 2997 12 99%
|
||||||
|
coverage gate: app/ 99% (>90%) OK
|
||||||
|
All checks passed!
|
||||||
|
0 errors, 0 warnings, 0 informations
|
||||||
|
validation OK
|
||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
**Report — phase 78, task 02 (regression sweep + commit)**
|
||||||
|
|
||||||
|
- E2E in isolation (DB up): `uv run pytest tests/e2e/test_background_no_motion.py -v --no-cov` → 5 passed; `uv run pytest tests/e2e/test_dark_tech_theme.py -v --no-cov` → 6 passed; `uv run pytest tests/e2e/test_smoke.py -v --no-cov` → 3 passed.
|
||||||
|
- Visual check (real server, no mock): booted two real uvicorn instances (built-in palette + `BOR_THEME=indigo.css`), captured 8 screenshots (chat + sources × default/reduced-motion × both palettes). Result: static 44px grid over the flat `--bg` canvas, zero glow spots, `document.getAnimations()` empty; default vs reduced-motion screenshots are **byte-identical**; indigo applies cleanly (`--bg` `rgb(10,14,26)`, grid follows `--line`).
|
||||||
|
- Full gate: `uv run pytest --cov=app --cov-report=term-missing` → 1561 passed, **99%** on `app/` (>90% ✓); `uv run ruff check .` → All checks passed; `uv run pyright` → 0 errors, 0 warnings.
|
||||||
|
- `rg "bg-glow" frontend/ tests/ app/` → zero hits; themes carry no keyframes/animation.
|
||||||
|
- **Commit/phase-move:** per the pipeline override (harness makes the one atomic `--no-gpg-sign` commit and moves the phase dir), I did **not** run `git add`/`git commit` or touch `.agents/phases/`; all changes left in the working tree.
|
||||||
|
- **Confirmed deviation (from task 01, validated here):** `test_dark_tech_theme.py` could not stay literally UNCHANGED — it pinned the deleted `bg-glow-a/b/c` fades, so it could never be green unchanged. Minimal contract-preserving update only: `test_animated_background` → `test_static_background` + the reduced-motion test; all four palette/emoji/behavior/no-CDN tests byte-identical.
|
||||||
|
- Next pending: `.agents/phases/todo/79_api_tokens/` (phase 78's remaining files go to `complete/` with the harness commit).
|
||||||
+78
@@ -0,0 +1,78 @@
|
|||||||
|
........................................................................ [ 4%]
|
||||||
|
........................................................................ [ 9%]
|
||||||
|
........................................................................ [ 13%]
|
||||||
|
........................................................................ [ 18%]
|
||||||
|
........................................................................ [ 23%]
|
||||||
|
........................................................................ [ 27%]
|
||||||
|
........................................................................ [ 32%]
|
||||||
|
........................................................................ [ 36%]
|
||||||
|
........................................................................ [ 41%]
|
||||||
|
........................................................................ [ 46%]
|
||||||
|
........................................................................ [ 50%]
|
||||||
|
........................................................................ [ 55%]
|
||||||
|
........................................................................ [ 59%]
|
||||||
|
........................................................................ [ 64%]
|
||||||
|
........................................................................ [ 69%]
|
||||||
|
........................................................................ [ 73%]
|
||||||
|
........................................................................ [ 78%]
|
||||||
|
........................................................................ [ 83%]
|
||||||
|
........................................................................ [ 87%]
|
||||||
|
........................................................................ [ 92%]
|
||||||
|
........................................................................ [ 96%]
|
||||||
|
................................................. [100%]
|
||||||
|
=============================== warnings summary ===============================
|
||||||
|
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
|
||||||
|
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
|
||||||
|
from starlette.testclient import TestClient as TestClient # noqa
|
||||||
|
|
||||||
|
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
|
||||||
|
================================ tests coverage ================================
|
||||||
|
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
|
||||||
|
|
||||||
|
Name Stmts Miss Cover
|
||||||
|
-----------------------------------------------
|
||||||
|
app/__init__.py 1 0 100%
|
||||||
|
app/api/__init__.py 0 0 100%
|
||||||
|
app/api/auth.py 22 0 100%
|
||||||
|
app/api/chat.py 177 0 100%
|
||||||
|
app/api/chats.py 110 0 100%
|
||||||
|
app/api/config.py 7 0 100%
|
||||||
|
app/api/doc_drafts.py 93 0 100%
|
||||||
|
app/api/docs.py 50 0 100%
|
||||||
|
app/api/git_sources.py 212 0 100%
|
||||||
|
app/api/health.py 10 0 100%
|
||||||
|
app/api/steering.py 42 0 100%
|
||||||
|
app/api/suggestions.py 8 0 100%
|
||||||
|
app/api/sync.py 101 0 100%
|
||||||
|
app/config.py 140 0 100%
|
||||||
|
app/core/__init__.py 0 0 100%
|
||||||
|
app/core/auth.py 20 0 100%
|
||||||
|
app/core/caching.py 108 0 100%
|
||||||
|
app/core/debugging.py 29 2 93%
|
||||||
|
app/core/docs_push.py 39 0 100%
|
||||||
|
app/core/logging.py 13 0 100%
|
||||||
|
app/db.py 21 0 100%
|
||||||
|
app/main.py 60 0 100%
|
||||||
|
app/models.py 86 0 100%
|
||||||
|
app/rag/__init__.py 0 0 100%
|
||||||
|
app/rag/agent.py 222 0 100%
|
||||||
|
app/rag/archive_upload.py 128 0 100%
|
||||||
|
app/rag/chunker.py 206 4 98%
|
||||||
|
app/rag/git_sources.py 14 0 100%
|
||||||
|
app/rag/importer.py 180 3 98%
|
||||||
|
app/rag/llm.py 216 0 100%
|
||||||
|
app/rag/overview.py 71 0 100%
|
||||||
|
app/rag/prompts.py 88 0 100%
|
||||||
|
app/rag/retriever.py 150 3 98%
|
||||||
|
app/rag/scaffolding.py 55 0 100%
|
||||||
|
app/rag/source_removal.py 41 0 100%
|
||||||
|
app/rag/sources_meta.py 16 0 100%
|
||||||
|
app/rag/suggestions.py 27 0 100%
|
||||||
|
app/rag/summarizer.py 24 0 100%
|
||||||
|
app/schemas.py 210 0 100%
|
||||||
|
-----------------------------------------------
|
||||||
|
TOTAL 2997 12 99%
|
||||||
|
coverage gate: app/ 99% (>90%) OK
|
||||||
|
All checks passed!
|
||||||
|
0 errors, 0 warnings, 0 informations
|
||||||
|
validation OK
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
Everything is green. This was a verification pass only — no code changes were needed. Final report:
|
||||||
|
|
||||||
|
**Phase 79 (API tokens) — final verification pass**
|
||||||
|
- Verified tasks 01–08 already complete in `complete/79_api_tokens/`; confirmed implementation matches the locked design (A3–A5): `api_tokens` model + migration `0012`, hash-only token service, admin tokens router, `POST /api/token-auth` + 3-role whoami, live-revoking `require_user` on chat/suggestions/document-content, frontend gate + localStorage cache, Tokens admin view.
|
||||||
|
- Test results:
|
||||||
|
- `uv run pytest tests/e2e/test_api_tokens.py -v --no-cov` → **9 passed** (all owner-sentence clauses pinned: anon lockout, shared open, UI generate, token-user chat+doc, reload cache, admin walls 403, sign-out clears key, revocation refusal, no-enumeration error)
|
||||||
|
- `uv run pytest --cov=app --cov-report=term-missing` → **1639 passed, 0 failed, coverage 99%** (>90% floor)
|
||||||
|
- `uv run ruff check . && uv run pyright` → **clean (0 errors)**
|
||||||
|
- E2E spot-checks green: `test_admin_auth`, `test_share_chat` (anonymous), `test_chat_rag`, `test_nav_switch_keeps_stream`, `test_smoke` + all 10 token-migrated chat suites
|
||||||
|
- Completion criteria: all met — story suite green in isolation; token-user E2E chat + cited doc / anon 401 + gate / shared open / admin surfaces 403 (asserted in the passing suite); cache-survives-reload, sign-out clears, revocation refused on next request + fresh login; full suite + coverage + lint green.
|
||||||
|
- Notable: commit + phase-dir move are the harness's (per this pass's rules); README "API tokens" section verified present; `.env.example` correctly untouched. No defects found.
|
||||||
|
- Next pending phase: **80_history_suggestion_chips**.
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
........................................................................ [ 4%]
|
||||||
|
........................................................................ [ 8%]
|
||||||
|
........................................................................ [ 13%]
|
||||||
|
........................................................................ [ 17%]
|
||||||
|
........................................................................ [ 22%]
|
||||||
|
........................................................................ [ 26%]
|
||||||
|
........................................................................ [ 30%]
|
||||||
|
........................................................................ [ 35%]
|
||||||
|
........................................................................ [ 39%]
|
||||||
|
........................................................................ [ 44%]
|
||||||
|
........................................................................ [ 48%]
|
||||||
|
........................................................................ [ 53%]
|
||||||
|
........................................................................ [ 57%]
|
||||||
|
........................................................................ [ 61%]
|
||||||
|
........................................................................ [ 66%]
|
||||||
|
........................................................................ [ 70%]
|
||||||
|
........................................................................ [ 75%]
|
||||||
|
........................................................................ [ 79%]
|
||||||
|
........................................................................ [ 84%]
|
||||||
|
........................................................................ [ 88%]
|
||||||
|
........................................................................ [ 92%]
|
||||||
|
........................................................................ [ 97%]
|
||||||
|
........................................... [100%]
|
||||||
|
=============================== warnings summary ===============================
|
||||||
|
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
|
||||||
|
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
|
||||||
|
from starlette.testclient import TestClient as TestClient # noqa
|
||||||
|
|
||||||
|
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
|
||||||
|
================================ tests coverage ================================
|
||||||
|
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
|
||||||
|
|
||||||
|
Name Stmts Miss Cover
|
||||||
|
-----------------------------------------------
|
||||||
|
app/__init__.py 1 0 100%
|
||||||
|
app/api/__init__.py 0 0 100%
|
||||||
|
app/api/auth.py 40 0 100%
|
||||||
|
app/api/chat.py 178 0 100%
|
||||||
|
app/api/chats.py 110 0 100%
|
||||||
|
app/api/config.py 7 0 100%
|
||||||
|
app/api/doc_drafts.py 93 0 100%
|
||||||
|
app/api/docs.py 50 0 100%
|
||||||
|
app/api/git_sources.py 212 0 100%
|
||||||
|
app/api/health.py 10 0 100%
|
||||||
|
app/api/steering.py 42 0 100%
|
||||||
|
app/api/suggestions.py 11 0 100%
|
||||||
|
app/api/sync.py 101 0 100%
|
||||||
|
app/api/tokens.py 28 0 100%
|
||||||
|
app/config.py 140 0 100%
|
||||||
|
app/core/__init__.py 0 0 100%
|
||||||
|
app/core/auth.py 45 0 100%
|
||||||
|
app/core/caching.py 108 0 100%
|
||||||
|
app/core/debugging.py 29 2 93%
|
||||||
|
app/core/docs_push.py 39 0 100%
|
||||||
|
app/core/logging.py 13 0 100%
|
||||||
|
app/core/tokens.py 33 0 100%
|
||||||
|
app/db.py 21 0 100%
|
||||||
|
app/main.py 62 0 100%
|
||||||
|
app/models.py 94 0 100%
|
||||||
|
app/rag/__init__.py 0 0 100%
|
||||||
|
app/rag/agent.py 222 0 100%
|
||||||
|
app/rag/archive_upload.py 128 0 100%
|
||||||
|
app/rag/chunker.py 206 4 98%
|
||||||
|
app/rag/git_sources.py 14 0 100%
|
||||||
|
app/rag/importer.py 180 3 98%
|
||||||
|
app/rag/llm.py 216 0 100%
|
||||||
|
app/rag/overview.py 71 0 100%
|
||||||
|
app/rag/prompts.py 88 0 100%
|
||||||
|
app/rag/retriever.py 150 3 98%
|
||||||
|
app/rag/scaffolding.py 55 0 100%
|
||||||
|
app/rag/source_removal.py 41 0 100%
|
||||||
|
app/rag/sources_meta.py 16 0 100%
|
||||||
|
app/rag/suggestions.py 27 0 100%
|
||||||
|
app/rag/summarizer.py 24 0 100%
|
||||||
|
app/schemas.py 231 0 100%
|
||||||
|
-----------------------------------------------
|
||||||
|
TOTAL 3136 12 99%
|
||||||
|
coverage gate: app/ 99% (>90%) OK
|
||||||
|
All checks passed!
|
||||||
|
0 errors, 0 warnings, 0 informations
|
||||||
|
validation OK
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
All green. Task 01 is complete.
|
||||||
|
|
||||||
|
**Report**
|
||||||
|
|
||||||
|
- `app/models.py`: `ApiToken` model after `SavedChat` — `id` UUID PK (uuid4 default), `label` String(120) NOT NULL (display-only), `token_hash` String(64) NOT NULL unique (sha256 of the full `bor_…` token), `created_at` server-default now(), `last_used_at`/`revoked_at` TIMESTAMPTZ NULL; docstring documents the hashed-credential trust model; module docstring gains the `api_tokens` bullet.
|
||||||
|
- `alembic/versions/0012_api_tokens.py` (revision `0012` → `0011`): `op.create_table` + explicit unique `op.create_index("ix_api_tokens_token_hash", …)` (the 0009/0011 shape); downgrade drops index then table.
|
||||||
|
- `tests/unit/test_api_tokens_model.py` (new, schema-level per house precedent): table name, column set/nullability/widths, `token_hash` unique, `label` not unique — 4 tests.
|
||||||
|
- `tests/integration/test_migration_0012.py` (new, mirrors `test_migration_0011.py`): upgrade column contract, lifecycle round-trip, duplicate-hash rejection + repeated-label OK, downgrade/round-trip — 5 tests.
|
||||||
|
- Migration: `uv run alembic upgrade head` → 0012 applied; `downgrade -1` + `upgrade head` round-trips cleanly; dev DB at `0012 (head)` so tasks 02/03 pick the table up.
|
||||||
|
- Full suite: `uv run pytest --cov=app --cov-report=term` → **1570 passed**, coverage **99%** (>90%; `app/models.py` 100%).
|
||||||
|
- Lint/types: `uv run ruff check .` → All checks passed; `uv run pyright` → 0 errors.
|
||||||
|
- Deviation (minor): model column uses `unique=True` alone (all four house precedents — `doc_drafts.token`, `saved_chats.share_token`, etc.) rather than `unique=True, index=True`; the unique index requirement is met by the migration's explicit `op.create_index(..., unique=True)`, exactly the 0009 shape the task pointed at.
|
||||||
|
- Next pending task: `.agents/phases/todo/79_api_tokens/02_token_admin_api.md` (token service + admin create/list/revoke).
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
........................................................................ [ 4%]
|
||||||
|
........................................................................ [ 9%]
|
||||||
|
........................................................................ [ 13%]
|
||||||
|
........................................................................ [ 18%]
|
||||||
|
........................................................................ [ 22%]
|
||||||
|
........................................................................ [ 27%]
|
||||||
|
........................................................................ [ 32%]
|
||||||
|
........................................................................ [ 36%]
|
||||||
|
........................................................................ [ 41%]
|
||||||
|
........................................................................ [ 45%]
|
||||||
|
........................................................................ [ 50%]
|
||||||
|
........................................................................ [ 55%]
|
||||||
|
........................................................................ [ 59%]
|
||||||
|
........................................................................ [ 64%]
|
||||||
|
........................................................................ [ 68%]
|
||||||
|
........................................................................ [ 73%]
|
||||||
|
........................................................................ [ 77%]
|
||||||
|
........................................................................ [ 82%]
|
||||||
|
........................................................................ [ 87%]
|
||||||
|
........................................................................ [ 91%]
|
||||||
|
........................................................................ [ 96%]
|
||||||
|
.......................................................... [100%]
|
||||||
|
=============================== warnings summary ===============================
|
||||||
|
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
|
||||||
|
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
|
||||||
|
from starlette.testclient import TestClient as TestClient # noqa
|
||||||
|
|
||||||
|
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
|
||||||
|
================================ tests coverage ================================
|
||||||
|
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
|
||||||
|
|
||||||
|
Name Stmts Miss Cover
|
||||||
|
-----------------------------------------------
|
||||||
|
app/__init__.py 1 0 100%
|
||||||
|
app/api/__init__.py 0 0 100%
|
||||||
|
app/api/auth.py 22 0 100%
|
||||||
|
app/api/chat.py 177 0 100%
|
||||||
|
app/api/chats.py 110 0 100%
|
||||||
|
app/api/config.py 7 0 100%
|
||||||
|
app/api/doc_drafts.py 93 0 100%
|
||||||
|
app/api/docs.py 50 0 100%
|
||||||
|
app/api/git_sources.py 212 0 100%
|
||||||
|
app/api/health.py 10 0 100%
|
||||||
|
app/api/steering.py 42 0 100%
|
||||||
|
app/api/suggestions.py 8 0 100%
|
||||||
|
app/api/sync.py 101 0 100%
|
||||||
|
app/config.py 140 0 100%
|
||||||
|
app/core/__init__.py 0 0 100%
|
||||||
|
app/core/auth.py 20 0 100%
|
||||||
|
app/core/caching.py 108 0 100%
|
||||||
|
app/core/debugging.py 29 2 93%
|
||||||
|
app/core/docs_push.py 39 0 100%
|
||||||
|
app/core/logging.py 13 0 100%
|
||||||
|
app/db.py 21 0 100%
|
||||||
|
app/main.py 60 0 100%
|
||||||
|
app/models.py 94 0 100%
|
||||||
|
app/rag/__init__.py 0 0 100%
|
||||||
|
app/rag/agent.py 222 0 100%
|
||||||
|
app/rag/archive_upload.py 128 0 100%
|
||||||
|
app/rag/chunker.py 206 4 98%
|
||||||
|
app/rag/git_sources.py 14 0 100%
|
||||||
|
app/rag/importer.py 180 3 98%
|
||||||
|
app/rag/llm.py 216 0 100%
|
||||||
|
app/rag/overview.py 71 0 100%
|
||||||
|
app/rag/prompts.py 88 0 100%
|
||||||
|
app/rag/retriever.py 150 3 98%
|
||||||
|
app/rag/scaffolding.py 55 0 100%
|
||||||
|
app/rag/source_removal.py 41 0 100%
|
||||||
|
app/rag/sources_meta.py 16 0 100%
|
||||||
|
app/rag/suggestions.py 27 0 100%
|
||||||
|
app/rag/summarizer.py 24 0 100%
|
||||||
|
app/schemas.py 210 0 100%
|
||||||
|
-----------------------------------------------
|
||||||
|
TOTAL 3005 12 99%
|
||||||
|
coverage gate: app/ 99% (>90%) OK
|
||||||
|
All checks passed!
|
||||||
|
0 errors, 0 warnings, 0 informations
|
||||||
|
validation OK
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
Task 02 complete — all gates green.
|
||||||
|
|
||||||
|
**Implemented**
|
||||||
|
- `app/core/tokens.py` — `generate_token` / `hash_token` (sha256 of FULL string) / `create_token` / `find_active_by_token` (hash → unique-index hit → `revoked_at IS NULL`, generic miss) / `mark_used` / `revoke` (stamp-once, False only for missing id); hash-lookup-vs-constant-time-contrast documented; house flush-not-commit split
|
||||||
|
- `app/schemas.py` — `TokenCreateRequest` (strip-then 1–120, 422 fail-loud), `TokenCreated` (only schema carrying plaintext), `TokenListItem` / `TokenList` (secret-free by construction), `TokenAuthRequest` (deliberately no min-length → task-03 401 contract)
|
||||||
|
- `app/api/tokens.py` — router-wide `require_admin`; `POST /tokens` → 201 (plaintext once), `GET /tokens` newest-first (`created_at desc, id desc`), `POST /tokens/{id}/revoke` → 204 idempotent / 404 `token not found`; registered in `app/main.py` before the static mount
|
||||||
|
- `tests/unit/test_tokens.py` (11) + `tests/integration/test_tokens_api.py` (7): shape, hash, round-trip, all miss paths, revoke idempotency + `last_used_at` preservation, anonymous 403s, plaintext/hash never in list, duplicate labels, 422s, 404, ordering
|
||||||
|
|
||||||
|
**Results**
|
||||||
|
- `uv run pytest --cov=app --cov-report=term-missing` → 1588 passed; TOTAL 99% (`core/tokens.py` 100%, `api/tokens.py` 100%)
|
||||||
|
- `uv run ruff check .` → clean (fixed `datetime.UTC` alias + import sort); `uv run pyright` → 0 errors
|
||||||
|
|
||||||
|
**Decisions** — service flushes, endpoint commits (sources_meta house pattern); used `datetime.UTC` per ruff (identical to spec's `timezone.utc`).
|
||||||
|
|
||||||
|
**Next pending task:** `03_token_auth_enforcement.md`
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
........................................................................ [ 4%]
|
||||||
|
........................................................................ [ 9%]
|
||||||
|
........................................................................ [ 13%]
|
||||||
|
........................................................................ [ 18%]
|
||||||
|
........................................................................ [ 22%]
|
||||||
|
........................................................................ [ 27%]
|
||||||
|
........................................................................ [ 31%]
|
||||||
|
........................................................................ [ 36%]
|
||||||
|
........................................................................ [ 40%]
|
||||||
|
........................................................................ [ 45%]
|
||||||
|
........................................................................ [ 49%]
|
||||||
|
........................................................................ [ 54%]
|
||||||
|
........................................................................ [ 58%]
|
||||||
|
........................................................................ [ 63%]
|
||||||
|
........................................................................ [ 68%]
|
||||||
|
........................................................................ [ 72%]
|
||||||
|
........................................................................ [ 77%]
|
||||||
|
........................................................................ [ 81%]
|
||||||
|
........................................................................ [ 86%]
|
||||||
|
........................................................................ [ 90%]
|
||||||
|
........................................................................ [ 95%]
|
||||||
|
........................................................................ [ 99%]
|
||||||
|
.... [100%]
|
||||||
|
=============================== warnings summary ===============================
|
||||||
|
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
|
||||||
|
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
|
||||||
|
from starlette.testclient import TestClient as TestClient # noqa
|
||||||
|
|
||||||
|
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
|
||||||
|
================================ tests coverage ================================
|
||||||
|
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
|
||||||
|
|
||||||
|
Name Stmts Miss Cover
|
||||||
|
-----------------------------------------------
|
||||||
|
app/__init__.py 1 0 100%
|
||||||
|
app/api/__init__.py 0 0 100%
|
||||||
|
app/api/auth.py 22 0 100%
|
||||||
|
app/api/chat.py 177 0 100%
|
||||||
|
app/api/chats.py 110 0 100%
|
||||||
|
app/api/config.py 7 0 100%
|
||||||
|
app/api/doc_drafts.py 93 0 100%
|
||||||
|
app/api/docs.py 50 0 100%
|
||||||
|
app/api/git_sources.py 212 0 100%
|
||||||
|
app/api/health.py 10 0 100%
|
||||||
|
app/api/steering.py 42 0 100%
|
||||||
|
app/api/suggestions.py 8 0 100%
|
||||||
|
app/api/sync.py 101 0 100%
|
||||||
|
app/api/tokens.py 28 0 100%
|
||||||
|
app/config.py 140 0 100%
|
||||||
|
app/core/__init__.py 0 0 100%
|
||||||
|
app/core/auth.py 20 0 100%
|
||||||
|
app/core/caching.py 108 0 100%
|
||||||
|
app/core/debugging.py 29 2 93%
|
||||||
|
app/core/docs_push.py 39 0 100%
|
||||||
|
app/core/logging.py 13 0 100%
|
||||||
|
app/core/tokens.py 33 0 100%
|
||||||
|
app/db.py 21 0 100%
|
||||||
|
app/main.py 62 0 100%
|
||||||
|
app/models.py 94 0 100%
|
||||||
|
app/rag/__init__.py 0 0 100%
|
||||||
|
app/rag/agent.py 222 0 100%
|
||||||
|
app/rag/archive_upload.py 128 0 100%
|
||||||
|
app/rag/chunker.py 206 4 98%
|
||||||
|
app/rag/git_sources.py 14 0 100%
|
||||||
|
app/rag/importer.py 180 3 98%
|
||||||
|
app/rag/llm.py 216 0 100%
|
||||||
|
app/rag/overview.py 71 0 100%
|
||||||
|
app/rag/prompts.py 88 0 100%
|
||||||
|
app/rag/retriever.py 150 3 98%
|
||||||
|
app/rag/scaffolding.py 55 0 100%
|
||||||
|
app/rag/source_removal.py 41 0 100%
|
||||||
|
app/rag/sources_meta.py 16 0 100%
|
||||||
|
app/rag/suggestions.py 27 0 100%
|
||||||
|
app/rag/summarizer.py 24 0 100%
|
||||||
|
app/schemas.py 231 0 100%
|
||||||
|
-----------------------------------------------
|
||||||
|
TOTAL 3089 12 99%
|
||||||
|
coverage gate: app/ 99% (>90%) OK
|
||||||
|
All checks passed!
|
||||||
|
0 errors, 0 warnings, 0 informations
|
||||||
|
validation OK
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
All green. Task 03 complete.
|
||||||
|
|
||||||
|
**Implemented**
|
||||||
|
- `app/core/auth.py`: `USER_SESSION_KEY`/`USER_TOKEN_ID_KEY` + `require_user` — admin always passes (no DB touch); token session live-checks the `api_tokens` row per request (PK lookup = revocation check); dead session (revoked/missing/corrupt id) → both user keys popped + 401 `authentication required`; anonymous → same 401
|
||||||
|
- `app/api/auth.py`: public `POST /api/token-auth` (valid → `mark_used`+commit+`user`/`user_token_id` session keys → 204; malformed/unknown/revoked/empty → ONE generic 401 `invalid token`); `whoami` now reports `admin`|`user`|`anonymous` (admin wins on coexistence); `WhoamiResponse.role` comment updated; logout unchanged (wipes both roles)
|
||||||
|
- Enforcement: `_user: None = Depends(require_user)` on `POST /api/chat`, `GET /api/suggestions` (+ `db` dep), `GET /api/documents/content` (docstring: phase-16 soft rule SUPERSEDED); fixed stale "viewer/chat public" docstrings in `docs.py`, `steering.py`, `models.py`, `tokens.py`
|
||||||
|
- Tests: `require_user` matrix + 3-role whoami in `tests/unit/test_auth.py`; `test_auth_api.py` rewritten pins (token-auth flows, mid-session revocation → chat 401 + whoami anonymous + fresh login refused, token-user surface matrix incl. 403s on all admin surfaces); admin sign-in wired into `test_api.py`, `test_chat_api.py`, `test_kb_overview_api.py`, `test_document_content.py`, `test_document_viewer.py`, `test_chat_gate.py`, `test_chat_cancel.py` (raw ASGI scope carries a signed admin cookie)
|
||||||
|
|
||||||
|
**Results**
|
||||||
|
- `uv run pytest --cov=app --cov-report=term-missing` → 1605 passed; TOTAL coverage **99%** (>90% bar; all modified files 100%)
|
||||||
|
- `uv run ruff check .` → All checks passed; `uv run pyright` → 0 errors
|
||||||
|
|
||||||
|
**Decisions**: `last_used_at` stamped only on `token-auth` (per task-03 spec — the only request presenting the token; the gate re-sends it per page load); prior tasks' docstrings aligned accordingly. E2E suites (10 anonymous chat suites, `test_admin_auth.py`) intentionally untouched — that's task 04.
|
||||||
|
|
||||||
|
**Next pending task**: `.agents/phases/todo/79_api_tokens/04_migrate_anonymous_e2e.md`
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
........................................................................ [ 4%]
|
||||||
|
........................................................................ [ 8%]
|
||||||
|
........................................................................ [ 13%]
|
||||||
|
........................................................................ [ 17%]
|
||||||
|
........................................................................ [ 22%]
|
||||||
|
........................................................................ [ 26%]
|
||||||
|
........................................................................ [ 31%]
|
||||||
|
........................................................................ [ 35%]
|
||||||
|
........................................................................ [ 40%]
|
||||||
|
........................................................................ [ 44%]
|
||||||
|
........................................................................ [ 49%]
|
||||||
|
........................................................................ [ 53%]
|
||||||
|
........................................................................ [ 58%]
|
||||||
|
........................................................................ [ 62%]
|
||||||
|
........................................................................ [ 67%]
|
||||||
|
........................................................................ [ 71%]
|
||||||
|
........................................................................ [ 76%]
|
||||||
|
........................................................................ [ 80%]
|
||||||
|
........................................................................ [ 85%]
|
||||||
|
........................................................................ [ 89%]
|
||||||
|
........................................................................ [ 94%]
|
||||||
|
........................................................................ [ 98%]
|
||||||
|
..................... [100%]
|
||||||
|
=============================== warnings summary ===============================
|
||||||
|
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
|
||||||
|
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
|
||||||
|
from starlette.testclient import TestClient as TestClient # noqa
|
||||||
|
|
||||||
|
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
|
||||||
|
================================ tests coverage ================================
|
||||||
|
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
|
||||||
|
|
||||||
|
Name Stmts Miss Cover
|
||||||
|
-----------------------------------------------
|
||||||
|
app/__init__.py 1 0 100%
|
||||||
|
app/api/__init__.py 0 0 100%
|
||||||
|
app/api/auth.py 40 0 100%
|
||||||
|
app/api/chat.py 178 0 100%
|
||||||
|
app/api/chats.py 110 0 100%
|
||||||
|
app/api/config.py 7 0 100%
|
||||||
|
app/api/doc_drafts.py 93 0 100%
|
||||||
|
app/api/docs.py 50 0 100%
|
||||||
|
app/api/git_sources.py 212 0 100%
|
||||||
|
app/api/health.py 10 0 100%
|
||||||
|
app/api/steering.py 42 0 100%
|
||||||
|
app/api/suggestions.py 11 0 100%
|
||||||
|
app/api/sync.py 101 0 100%
|
||||||
|
app/api/tokens.py 28 0 100%
|
||||||
|
app/config.py 140 0 100%
|
||||||
|
app/core/__init__.py 0 0 100%
|
||||||
|
app/core/auth.py 45 0 100%
|
||||||
|
app/core/caching.py 108 0 100%
|
||||||
|
app/core/debugging.py 29 2 93%
|
||||||
|
app/core/docs_push.py 39 0 100%
|
||||||
|
app/core/logging.py 13 0 100%
|
||||||
|
app/core/tokens.py 33 0 100%
|
||||||
|
app/db.py 21 0 100%
|
||||||
|
app/main.py 62 0 100%
|
||||||
|
app/models.py 94 0 100%
|
||||||
|
app/rag/__init__.py 0 0 100%
|
||||||
|
app/rag/agent.py 222 0 100%
|
||||||
|
app/rag/archive_upload.py 128 0 100%
|
||||||
|
app/rag/chunker.py 206 4 98%
|
||||||
|
app/rag/git_sources.py 14 0 100%
|
||||||
|
app/rag/importer.py 180 3 98%
|
||||||
|
app/rag/llm.py 216 0 100%
|
||||||
|
app/rag/overview.py 71 0 100%
|
||||||
|
app/rag/prompts.py 88 0 100%
|
||||||
|
app/rag/retriever.py 150 3 98%
|
||||||
|
app/rag/scaffolding.py 55 0 100%
|
||||||
|
app/rag/source_removal.py 41 0 100%
|
||||||
|
app/rag/sources_meta.py 16 0 100%
|
||||||
|
app/rag/suggestions.py 27 0 100%
|
||||||
|
app/rag/summarizer.py 24 0 100%
|
||||||
|
app/schemas.py 231 0 100%
|
||||||
|
-----------------------------------------------
|
||||||
|
TOTAL 3136 12 99%
|
||||||
|
coverage gate: app/ 99% (>90%) OK
|
||||||
|
All checks passed!
|
||||||
|
0 errors, 0 warnings, 0 informations
|
||||||
|
validation OK
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
All gates pass. Final state verified:
|
||||||
|
|
||||||
|
**Task 04 report — E2E suites meet the new auth contract**
|
||||||
|
|
||||||
|
**Implemented**
|
||||||
|
- `auth_helpers.py`: added `login_with_token` (+ `WRONG_TOKEN` sentinel, wrong/revoked branch via `#auth-gate-error` role=alert) for task 07; made `login()` idempotent — an already-signed-in context boot-redirects past the form (login.js whoami), which was breaking double sign-in.
|
||||||
|
- Signed in the 10 named chat suites **plus ~40 more** the audit widened (viewer/suggestion/upload/sync suites) — page `login()` or httpx client login; shared-chat + deliberate anonymous-surface suites untouched.
|
||||||
|
- `test_admin_auth.py`: anonymous pins moved to the gated contract (401 `{"detail":"authentication required"}` on `/api/chat` + `/api/documents/content`, whoami anonymous, viewer not-found card); password flow unchanged.
|
||||||
|
- **Defects fixed**: (a) app bug — `sources.js` `loadDocs` race (boot re-attach + boot load interleave → 2× catalog rows, 108-row table) got a monotonic seq guard; (b) test-infra bug — 8 module-scoped `app_server` overrides collided with the session app on port 8123 in combined runs (silently driving the wrong server) → dedicated ports 8124–8134; (c) sync suites' TRUNCATE now clears `git_sources` (shared-DB hygiene); (d) anonymous viewer-content asserts made role-conditional (not-found card = anonymous settled state); (e) anonymous chip/contrast tests sign in (suggestions gated).
|
||||||
|
|
||||||
|
**Gates**
|
||||||
|
- `uv run pytest tests/e2e -v --no-cov` → **308 passed, 0 failed** (single session; baseline was 174 failed)
|
||||||
|
- `uv run pytest tests/e2e/test_share_chat.py test_smoke.py test_nav_switch_keeps_stream.py test_archive_upload_sources.py test_admin_auth.py --no-cov` (isolation spot-check) → 22 passed
|
||||||
|
- `uv run pytest --cov=app --cov-report=term-missing` → **1605 passed, app/ 99%**
|
||||||
|
- `uv run ruff check . && uv run pyright` → clean
|
||||||
|
|
||||||
|
**Decisions**: port fix + loadDocs guard are test-infra/app defects (no story semantics changed), required to make the combined inventory green — the full inventory had never been run as one session before. Not committed (per instructions). Next pending: **05_frontend_token_gate**.
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
........................................................................ [ 4%]
|
||||||
|
........................................................................ [ 8%]
|
||||||
|
........................................................................ [ 13%]
|
||||||
|
........................................................................ [ 17%]
|
||||||
|
........................................................................ [ 22%]
|
||||||
|
........................................................................ [ 26%]
|
||||||
|
........................................................................ [ 31%]
|
||||||
|
........................................................................ [ 35%]
|
||||||
|
........................................................................ [ 40%]
|
||||||
|
........................................................................ [ 44%]
|
||||||
|
........................................................................ [ 49%]
|
||||||
|
........................................................................ [ 53%]
|
||||||
|
........................................................................ [ 58%]
|
||||||
|
........................................................................ [ 62%]
|
||||||
|
........................................................................ [ 67%]
|
||||||
|
........................................................................ [ 71%]
|
||||||
|
........................................................................ [ 76%]
|
||||||
|
........................................................................ [ 80%]
|
||||||
|
........................................................................ [ 85%]
|
||||||
|
........................................................................ [ 89%]
|
||||||
|
........................................................................ [ 94%]
|
||||||
|
........................................................................ [ 98%]
|
||||||
|
..................... [100%]
|
||||||
|
=============================== warnings summary ===============================
|
||||||
|
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
|
||||||
|
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
|
||||||
|
from starlette.testclient import TestClient as TestClient # noqa
|
||||||
|
|
||||||
|
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
|
||||||
|
================================ tests coverage ================================
|
||||||
|
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
|
||||||
|
|
||||||
|
Name Stmts Miss Cover
|
||||||
|
-----------------------------------------------
|
||||||
|
app/__init__.py 1 0 100%
|
||||||
|
app/api/__init__.py 0 0 100%
|
||||||
|
app/api/auth.py 40 0 100%
|
||||||
|
app/api/chat.py 178 0 100%
|
||||||
|
app/api/chats.py 110 0 100%
|
||||||
|
app/api/config.py 7 0 100%
|
||||||
|
app/api/doc_drafts.py 93 0 100%
|
||||||
|
app/api/docs.py 50 0 100%
|
||||||
|
app/api/git_sources.py 212 0 100%
|
||||||
|
app/api/health.py 10 0 100%
|
||||||
|
app/api/steering.py 42 0 100%
|
||||||
|
app/api/suggestions.py 11 0 100%
|
||||||
|
app/api/sync.py 101 0 100%
|
||||||
|
app/api/tokens.py 28 0 100%
|
||||||
|
app/config.py 140 0 100%
|
||||||
|
app/core/__init__.py 0 0 100%
|
||||||
|
app/core/auth.py 45 0 100%
|
||||||
|
app/core/caching.py 108 0 100%
|
||||||
|
app/core/debugging.py 29 2 93%
|
||||||
|
app/core/docs_push.py 39 0 100%
|
||||||
|
app/core/logging.py 13 0 100%
|
||||||
|
app/core/tokens.py 33 0 100%
|
||||||
|
app/db.py 21 0 100%
|
||||||
|
app/main.py 62 0 100%
|
||||||
|
app/models.py 94 0 100%
|
||||||
|
app/rag/__init__.py 0 0 100%
|
||||||
|
app/rag/agent.py 222 0 100%
|
||||||
|
app/rag/archive_upload.py 128 0 100%
|
||||||
|
app/rag/chunker.py 206 4 98%
|
||||||
|
app/rag/git_sources.py 14 0 100%
|
||||||
|
app/rag/importer.py 180 3 98%
|
||||||
|
app/rag/llm.py 216 0 100%
|
||||||
|
app/rag/overview.py 71 0 100%
|
||||||
|
app/rag/prompts.py 88 0 100%
|
||||||
|
app/rag/retriever.py 150 3 98%
|
||||||
|
app/rag/scaffolding.py 55 0 100%
|
||||||
|
app/rag/source_removal.py 41 0 100%
|
||||||
|
app/rag/sources_meta.py 16 0 100%
|
||||||
|
app/rag/suggestions.py 27 0 100%
|
||||||
|
app/rag/summarizer.py 24 0 100%
|
||||||
|
app/schemas.py 231 0 100%
|
||||||
|
-----------------------------------------------
|
||||||
|
TOTAL 3136 12 99%
|
||||||
|
coverage gate: app/ 99% (>90%) OK
|
||||||
|
All checks passed!
|
||||||
|
0 errors, 0 warnings, 0 informations
|
||||||
|
validation OK
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
All work is complete and verified. Final report:
|
||||||
|
|
||||||
|
**Implemented** — In-app token gate + browser caching (phase 79 task 05):
|
||||||
|
- `frontend/assets/token-gate.js` (new): mounts `#auth-gate` (shell) / `#doc-auth-gate` (viewer). Silent re-auth of cached `bor.token` via `POST /api/token-auth` runs **before** the whoami check (no gate flash, race-free vs header boot); dead/revoked cached tokens are dropped. Anonymous → gate revealed, `#main` made `inert`, focus lands on the token input. Success → cache token, invalidate whoami cache, unlock **without reload**, re-boot header, fire `onAuthed`.
|
||||||
|
- `header.js`: Sign in/out pair keys off `authenticated` (a token user sees Sign out; sign-out clears `bor.token` too); admin-only nav links + steering panel still key off `role === "admin"`.
|
||||||
|
- Gate markup/CSS in `index.html` + `document.html`; `Containerfile` esbuild line; unit pins `tests/unit/test_token_gate.py` (+ updated header/chat-UI pins); migrated 10 anonymous E2E suites to the gate contract.
|
||||||
|
|
||||||
|
**Test/lint/coverage**
|
||||||
|
- `uv run pytest --cov=app` → **1622 passed**, TOTAL coverage **99%** (gate >90%)
|
||||||
|
- `uv run ruff check .` → clean; `uv run pyright` → 0 errors
|
||||||
|
- E2E full inventory run: 4 anonymous-flow failures found → fixed → green; gate-related batch re-ran **61 passed**
|
||||||
|
- Real-server manual pass (9 flows: anonymous, wrong token, right token no-reload, cached reload, viewer, sign-out, revoked-login, revoked-cached, anonymous viewer) — all green
|
||||||
|
|
||||||
|
**Decisions** — Gate locks the whole `#main` (all views, incl. tuning/sources), not just chat; header stays interactive (admin form login remains the second entry). Token `user` = authenticated for the sign-in pair but gets the anonymous UI surface (no admin links, no steering panel). `test_global_tuning`'s 403-form contract now runs as a non-admin token user (the anonymous path is unreachable by design); `test_responsive_polish` checks the gate's focus contract anonymous + original AC3 contract signed-in. No git ops; no `.agents/` files touched; throwaway debug scripts removed.
|
||||||
|
|
||||||
|
**Next task** — `.agents/phases/todo/79_api_tokens/06_tokens_admin_view.md`
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
........................................................................ [ 4%]
|
||||||
|
........................................................................ [ 8%]
|
||||||
|
........................................................................ [ 13%]
|
||||||
|
........................................................................ [ 17%]
|
||||||
|
........................................................................ [ 22%]
|
||||||
|
........................................................................ [ 26%]
|
||||||
|
........................................................................ [ 31%]
|
||||||
|
........................................................................ [ 35%]
|
||||||
|
........................................................................ [ 39%]
|
||||||
|
........................................................................ [ 44%]
|
||||||
|
........................................................................ [ 48%]
|
||||||
|
........................................................................ [ 53%]
|
||||||
|
........................................................................ [ 57%]
|
||||||
|
........................................................................ [ 62%]
|
||||||
|
........................................................................ [ 66%]
|
||||||
|
........................................................................ [ 71%]
|
||||||
|
........................................................................ [ 75%]
|
||||||
|
........................................................................ [ 79%]
|
||||||
|
........................................................................ [ 84%]
|
||||||
|
........................................................................ [ 88%]
|
||||||
|
........................................................................ [ 93%]
|
||||||
|
........................................................................ [ 97%]
|
||||||
|
...................................... [100%]
|
||||||
|
=============================== warnings summary ===============================
|
||||||
|
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
|
||||||
|
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
|
||||||
|
from starlette.testclient import TestClient as TestClient # noqa
|
||||||
|
|
||||||
|
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
|
||||||
|
================================ tests coverage ================================
|
||||||
|
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
|
||||||
|
|
||||||
|
Name Stmts Miss Cover
|
||||||
|
-----------------------------------------------
|
||||||
|
app/__init__.py 1 0 100%
|
||||||
|
app/api/__init__.py 0 0 100%
|
||||||
|
app/api/auth.py 40 0 100%
|
||||||
|
app/api/chat.py 178 0 100%
|
||||||
|
app/api/chats.py 110 0 100%
|
||||||
|
app/api/config.py 7 0 100%
|
||||||
|
app/api/doc_drafts.py 93 0 100%
|
||||||
|
app/api/docs.py 50 0 100%
|
||||||
|
app/api/git_sources.py 212 0 100%
|
||||||
|
app/api/health.py 10 0 100%
|
||||||
|
app/api/steering.py 42 0 100%
|
||||||
|
app/api/suggestions.py 11 0 100%
|
||||||
|
app/api/sync.py 101 0 100%
|
||||||
|
app/api/tokens.py 28 0 100%
|
||||||
|
app/config.py 140 0 100%
|
||||||
|
app/core/__init__.py 0 0 100%
|
||||||
|
app/core/auth.py 45 0 100%
|
||||||
|
app/core/caching.py 108 0 100%
|
||||||
|
app/core/debugging.py 29 2 93%
|
||||||
|
app/core/docs_push.py 39 0 100%
|
||||||
|
app/core/logging.py 13 0 100%
|
||||||
|
app/core/tokens.py 33 0 100%
|
||||||
|
app/db.py 21 0 100%
|
||||||
|
app/main.py 62 0 100%
|
||||||
|
app/models.py 94 0 100%
|
||||||
|
app/rag/__init__.py 0 0 100%
|
||||||
|
app/rag/agent.py 222 0 100%
|
||||||
|
app/rag/archive_upload.py 128 0 100%
|
||||||
|
app/rag/chunker.py 206 4 98%
|
||||||
|
app/rag/git_sources.py 14 0 100%
|
||||||
|
app/rag/importer.py 180 3 98%
|
||||||
|
app/rag/llm.py 216 0 100%
|
||||||
|
app/rag/overview.py 71 0 100%
|
||||||
|
app/rag/prompts.py 88 0 100%
|
||||||
|
app/rag/retriever.py 150 3 98%
|
||||||
|
app/rag/scaffolding.py 55 0 100%
|
||||||
|
app/rag/source_removal.py 41 0 100%
|
||||||
|
app/rag/sources_meta.py 16 0 100%
|
||||||
|
app/rag/suggestions.py 27 0 100%
|
||||||
|
app/rag/summarizer.py 24 0 100%
|
||||||
|
app/schemas.py 231 0 100%
|
||||||
|
-----------------------------------------------
|
||||||
|
TOTAL 3136 12 99%
|
||||||
|
coverage gate: app/ 99% (>90%) OK
|
||||||
|
All checks passed!
|
||||||
|
0 errors, 0 warnings, 0 informations
|
||||||
|
validation OK
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
Everything is green and the tree contains only expected changes (plus the phase's earlier uncommitted tasks 01–05, which the harness commits atomically).
|
||||||
|
|
||||||
|
## Report — Task 06: admin Tokens view (generate · list · revoke)
|
||||||
|
|
||||||
|
**Implemented**
|
||||||
|
- `#view-tokens` section in the shell (`index.html`): page-head, `#tokens-gate` (history-gate pattern, `?next=/tokens.html`), `#tokens-status` live region, create row (label + Generate, ship-hidden), `#token-once` block (mono read-only field + Copy, ship-hidden), full-width table Label | Created | Last used | Status (Active em-dash / rose Revoked pill, cell aria-labels) | Actions (visually-hidden header), hidden empty row
|
||||||
|
- `frontend/assets/tokens.js`: `mount(root)` with admin gate (anonymous: gate in, controls out, **no** `/api/tokens` fetch), re-entrant `loadTokens()` (replaces rows, hides+wipes the once-block), generate (blank label → `"token"`, "Generating…" lifecycle, plaintext revealed only after the re-render load — never stored client-side), copy (clipboard + inline select-fallback, own ~10-line copy), inline two-step Revoke (`history-confirm-*`, row re-renders Revoked), `bor:view-refresh` listener armed in admin branch, all cells textContent (zero innerHTML)
|
||||||
|
- Router entries (`VIEW`/`VIEW_PATH`/`VIEW_MODULES`/`TITLES`/`DESCRIPTIONS`, replaceAll brand contract), `header.js` reveals `#nav-tokens` for admin
|
||||||
|
- `/tokens.html` in `app/main.py` `_shell_routes` + `caching.py` `HTML_PAGES`; styles.css Tokens section (history-table/brand-pill/stale-pill language, focus-visible, AA pairs) + ≤640px stacking
|
||||||
|
- Tests: `test_frontend_router.py` view-map/module/title pins extended + 2 new tokens contract tests; `test_api.py` (3 lists), `test_caching_revalidation.py` `SHELL_BACKED_PAGES`, `test_caching.py` `HTML_PAGES`; `test_nav_rename_sources.py` `NAV_TAIL` gains "Tokens"
|
||||||
|
- **Deviation (required):** `#nav-tokens` also ships on `document.html`/`login.html`/`shared.html` (task named only `index.html`) — the phase-34 one-bar contract and `test_nav_consistency.py`'s cross-page inventory parity (History precedent, 4 pages) make the shell-only link a real failure; null-safe header.js is unaffected
|
||||||
|
|
||||||
|
**Results**
|
||||||
|
- `uv run pytest --cov=app --cov-report=term-missing` → exit 0, TOTAL 99% (>90%; `main.py`/`caching.py`/`tokens.py` all 100%)
|
||||||
|
- `uv run ruff check . && uv run pyright` → clean
|
||||||
|
- E2E (isolated): temp manual pass (anon gate/no-fetch; admin generate→once+copy→revoke two-step→Revoked; re-show hides once-block; revoked token refused; active token unlocks; user gets gate) + `test_cache_busting`, `test_asset_cache_revalidation`, `test_nav_switch_keeps_stream`, `test_nav_consistency`, `test_admin_auth`, `test_nav_rename_sources`, `test_header_consistency`, `test_mobile_hamburger_nav`, `test_shared_header`, `test_navbar_refresh`, `test_tuning_nav_link`, `test_sticky_navbar`, `test_smoke`, `test_document_viewer`, `test_global_tuning`, `test_git_sources_admin`, `test_kb_overview`, `test_chat_history`, `test_history_copy`, `test_share_chat`, `test_stale_saved_chats` — all green (temp file deleted)
|
||||||
|
|
||||||
|
**Next pending task:** `07_e2e_story_suite.md`
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
........................................................................ [ 4%]
|
||||||
|
........................................................................ [ 8%]
|
||||||
|
........................................................................ [ 13%]
|
||||||
|
........................................................................ [ 17%]
|
||||||
|
........................................................................ [ 22%]
|
||||||
|
........................................................................ [ 26%]
|
||||||
|
........................................................................ [ 30%]
|
||||||
|
........................................................................ [ 35%]
|
||||||
|
........................................................................ [ 39%]
|
||||||
|
........................................................................ [ 44%]
|
||||||
|
........................................................................ [ 48%]
|
||||||
|
........................................................................ [ 53%]
|
||||||
|
........................................................................ [ 57%]
|
||||||
|
........................................................................ [ 61%]
|
||||||
|
........................................................................ [ 66%]
|
||||||
|
........................................................................ [ 70%]
|
||||||
|
........................................................................ [ 75%]
|
||||||
|
........................................................................ [ 79%]
|
||||||
|
........................................................................ [ 84%]
|
||||||
|
........................................................................ [ 88%]
|
||||||
|
........................................................................ [ 92%]
|
||||||
|
........................................................................ [ 97%]
|
||||||
|
........................................... [100%]
|
||||||
|
=============================== warnings summary ===============================
|
||||||
|
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
|
||||||
|
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
|
||||||
|
from starlette.testclient import TestClient as TestClient # noqa
|
||||||
|
|
||||||
|
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
|
||||||
|
================================ tests coverage ================================
|
||||||
|
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
|
||||||
|
|
||||||
|
Name Stmts Miss Cover
|
||||||
|
-----------------------------------------------
|
||||||
|
app/__init__.py 1 0 100%
|
||||||
|
app/api/__init__.py 0 0 100%
|
||||||
|
app/api/auth.py 40 0 100%
|
||||||
|
app/api/chat.py 178 0 100%
|
||||||
|
app/api/chats.py 110 0 100%
|
||||||
|
app/api/config.py 7 0 100%
|
||||||
|
app/api/doc_drafts.py 93 0 100%
|
||||||
|
app/api/docs.py 50 0 100%
|
||||||
|
app/api/git_sources.py 212 0 100%
|
||||||
|
app/api/health.py 10 0 100%
|
||||||
|
app/api/steering.py 42 0 100%
|
||||||
|
app/api/suggestions.py 11 0 100%
|
||||||
|
app/api/sync.py 101 0 100%
|
||||||
|
app/api/tokens.py 28 0 100%
|
||||||
|
app/config.py 140 0 100%
|
||||||
|
app/core/__init__.py 0 0 100%
|
||||||
|
app/core/auth.py 45 0 100%
|
||||||
|
app/core/caching.py 108 0 100%
|
||||||
|
app/core/debugging.py 29 2 93%
|
||||||
|
app/core/docs_push.py 39 0 100%
|
||||||
|
app/core/logging.py 13 0 100%
|
||||||
|
app/core/tokens.py 33 0 100%
|
||||||
|
app/db.py 21 0 100%
|
||||||
|
app/main.py 62 0 100%
|
||||||
|
app/models.py 94 0 100%
|
||||||
|
app/rag/__init__.py 0 0 100%
|
||||||
|
app/rag/agent.py 222 0 100%
|
||||||
|
app/rag/archive_upload.py 128 0 100%
|
||||||
|
app/rag/chunker.py 206 4 98%
|
||||||
|
app/rag/git_sources.py 14 0 100%
|
||||||
|
app/rag/importer.py 180 3 98%
|
||||||
|
app/rag/llm.py 216 0 100%
|
||||||
|
app/rag/overview.py 71 0 100%
|
||||||
|
app/rag/prompts.py 88 0 100%
|
||||||
|
app/rag/retriever.py 150 3 98%
|
||||||
|
app/rag/scaffolding.py 55 0 100%
|
||||||
|
app/rag/source_removal.py 41 0 100%
|
||||||
|
app/rag/sources_meta.py 16 0 100%
|
||||||
|
app/rag/suggestions.py 27 0 100%
|
||||||
|
app/rag/summarizer.py 24 0 100%
|
||||||
|
app/schemas.py 231 0 100%
|
||||||
|
-----------------------------------------------
|
||||||
|
TOTAL 3136 12 99%
|
||||||
|
coverage gate: app/ 99% (>90%) OK
|
||||||
|
All checks passed!
|
||||||
|
0 errors, 0 warnings, 0 informations
|
||||||
|
validation OK
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
All gates green. Task complete.
|
||||||
|
|
||||||
|
**Report — Phase 79, task 07 (story E2E suite)**
|
||||||
|
|
||||||
|
- Implemented `tests/e2e/test_api_tokens.py` (new, 9 tests) pinning every clause of TODO.md L5 in a real browser:
|
||||||
|
- anonymous lockout: `#auth-gate` visible, `#main` inert, composer unreachable via inverted tab walk; chat/suggestions/document-content 401 from empty cookies
|
||||||
|
- shared chats stay open: admin creates+shares via API; fresh context renders `/shared/<token>` with zero gate markup
|
||||||
|
- admin UI generate: `#token-once-value` matches `bor_[0-9a-f]{32}`, Active row in table, once-block wiped on re-show (router re-show refresh)
|
||||||
|
- token flow: real-gate login (task-04 helper) → grounded mock-LLM turn → cited chip opens same-page `#doc-modal`; all 5 admin nav links absent, Sign out visible, whoami role `user`
|
||||||
|
- caching: `localStorage["bor.token"]` survives reload with silent re-auth (no gate, no re-entry)
|
||||||
|
- admin-only walls 403 on tokens/chats/docs/steering/git-sources; sign out clears session + cached token
|
||||||
|
- revocation: UI two-step; chain pinned in the holder's own browser cookies — whoami still `user` (lazy) → next gated 401 clears the session → whoami anonymous; fresh login refused at the gate
|
||||||
|
- wrong token: role=alert stays, malformed and well-formed-unknown 401 bodies byte-equal (no enumeration)
|
||||||
|
- Results: `uv run pytest tests/e2e/test_api_tokens.py -v --no-cov` → **9 passed** (isolation); spot-check `test_admin_auth.py` + `test_share_chat.py` → 10 passed; `uv run pytest --cov=app` → green, `app/` coverage **99%** (>90%); `uv run ruff check .` clean; `uv run pyright` 0 errors
|
||||||
|
- Notable: no app defects found — tasks 01–06 hold. One harness quirk worked around: httpx's cookie jar ignores the expired `Set-Cookie` the 401 sends, so the revocation chain is driven through the user context's real browser fetch (also matches the task's "context cookies" wording; app contract itself verified via TestClient)
|
||||||
|
- DB hygiene: autouse fixture deletes only `e2e-`-labeled token rows (never TRUNCATE — shared dev DB); shared-chat row cleaned in `finally`
|
||||||
|
- Next pending task: `08_regression_sweep_commit.md` (full pipeline + README auth section + atomic commit)
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
........................................................................ [ 4%]
|
||||||
|
........................................................................ [ 8%]
|
||||||
|
........................................................................ [ 13%]
|
||||||
|
........................................................................ [ 17%]
|
||||||
|
........................................................................ [ 22%]
|
||||||
|
........................................................................ [ 26%]
|
||||||
|
........................................................................ [ 30%]
|
||||||
|
........................................................................ [ 35%]
|
||||||
|
........................................................................ [ 39%]
|
||||||
|
........................................................................ [ 44%]
|
||||||
|
........................................................................ [ 48%]
|
||||||
|
........................................................................ [ 53%]
|
||||||
|
........................................................................ [ 57%]
|
||||||
|
........................................................................ [ 61%]
|
||||||
|
........................................................................ [ 66%]
|
||||||
|
........................................................................ [ 70%]
|
||||||
|
........................................................................ [ 75%]
|
||||||
|
........................................................................ [ 79%]
|
||||||
|
........................................................................ [ 84%]
|
||||||
|
........................................................................ [ 88%]
|
||||||
|
........................................................................ [ 92%]
|
||||||
|
........................................................................ [ 97%]
|
||||||
|
........................................... [100%]
|
||||||
|
=============================== warnings summary ===============================
|
||||||
|
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
|
||||||
|
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
|
||||||
|
from starlette.testclient import TestClient as TestClient # noqa
|
||||||
|
|
||||||
|
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
|
||||||
|
================================ tests coverage ================================
|
||||||
|
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
|
||||||
|
|
||||||
|
Name Stmts Miss Cover
|
||||||
|
-----------------------------------------------
|
||||||
|
app/__init__.py 1 0 100%
|
||||||
|
app/api/__init__.py 0 0 100%
|
||||||
|
app/api/auth.py 40 0 100%
|
||||||
|
app/api/chat.py 178 0 100%
|
||||||
|
app/api/chats.py 110 0 100%
|
||||||
|
app/api/config.py 7 0 100%
|
||||||
|
app/api/doc_drafts.py 93 0 100%
|
||||||
|
app/api/docs.py 50 0 100%
|
||||||
|
app/api/git_sources.py 212 0 100%
|
||||||
|
app/api/health.py 10 0 100%
|
||||||
|
app/api/steering.py 42 0 100%
|
||||||
|
app/api/suggestions.py 11 0 100%
|
||||||
|
app/api/sync.py 101 0 100%
|
||||||
|
app/api/tokens.py 28 0 100%
|
||||||
|
app/config.py 140 0 100%
|
||||||
|
app/core/__init__.py 0 0 100%
|
||||||
|
app/core/auth.py 45 0 100%
|
||||||
|
app/core/caching.py 108 0 100%
|
||||||
|
app/core/debugging.py 29 2 93%
|
||||||
|
app/core/docs_push.py 39 0 100%
|
||||||
|
app/core/logging.py 13 0 100%
|
||||||
|
app/core/tokens.py 33 0 100%
|
||||||
|
app/db.py 21 0 100%
|
||||||
|
app/main.py 62 0 100%
|
||||||
|
app/models.py 94 0 100%
|
||||||
|
app/rag/__init__.py 0 0 100%
|
||||||
|
app/rag/agent.py 222 0 100%
|
||||||
|
app/rag/archive_upload.py 128 0 100%
|
||||||
|
app/rag/chunker.py 206 4 98%
|
||||||
|
app/rag/git_sources.py 14 0 100%
|
||||||
|
app/rag/importer.py 180 3 98%
|
||||||
|
app/rag/llm.py 216 0 100%
|
||||||
|
app/rag/overview.py 71 0 100%
|
||||||
|
app/rag/prompts.py 88 0 100%
|
||||||
|
app/rag/retriever.py 150 3 98%
|
||||||
|
app/rag/scaffolding.py 55 0 100%
|
||||||
|
app/rag/source_removal.py 41 0 100%
|
||||||
|
app/rag/sources_meta.py 16 0 100%
|
||||||
|
app/rag/suggestions.py 27 0 100%
|
||||||
|
app/rag/summarizer.py 24 0 100%
|
||||||
|
app/schemas.py 231 0 100%
|
||||||
|
-----------------------------------------------
|
||||||
|
TOTAL 3136 12 99%
|
||||||
|
coverage gate: app/ 99% (>90%) OK
|
||||||
|
All checks passed!
|
||||||
|
0 errors, 0 warnings, 0 informations
|
||||||
|
validation OK
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
All green. Final report:
|
||||||
|
|
||||||
|
**Phase 79, Task 08 — Regression sweep (commit owned by harness per pipeline rules)**
|
||||||
|
|
||||||
|
- Full suite: `uv run pytest --cov=app --cov-report=term` → **1627 passed, 99% coverage** (>90%; token deltas at 100%)
|
||||||
|
- `uv run ruff check . && uv run pyright` → clean (0 errors)
|
||||||
|
- E2E spot-checks, each in isolation: `test_api_tokens.py` 9✓, `test_admin_auth.py` 6✓, `test_share_chat.py` 4✓ (anonymous), `test_chat_rag.py` 3✓, `test_nav_switch_keeps_stream.py` 4✓, `test_smoke.py` 3✓
|
||||||
|
- Real-browser + real-LLM verification (3 fresh windows, live server on :8085): admin generated a token in the Tokens view (plaintext shown once, gone after reload); private window passed the gate, chatted, opened a cited doc, **reloaded without re-entry** (cached token); anonymous window met the gate yet opened a shared chat; admin revoked → user's next question 401'd live and a fresh login got the generic invalid-token error; token user 403'd on `/api/docs`, `/api/chats`
|
||||||
|
- README: new **API tokens** subsection under Admin & sign-in (generate/use/scope/revoke) + fixed two stale "viewer stays public" statements and the whoami/who-can-do-what tables
|
||||||
|
- `.env.example` unchanged — no new settings added (verified); dev-DB verification artifacts cleaned (tokens + 2 test chats removed)
|
||||||
|
- No defects found in prior tasks; no code changes needed
|
||||||
|
- Per harness rules: no `git add`/`commit`, phase dir left in `todo/` for the harness
|
||||||
|
- Next pending: phase 80 (`.agents/phases/todo/80_*/`), which builds on the now-gated `/api/suggestions`
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
........................................................................ [ 4%]
|
||||||
|
........................................................................ [ 8%]
|
||||||
|
........................................................................ [ 13%]
|
||||||
|
........................................................................ [ 17%]
|
||||||
|
........................................................................ [ 22%]
|
||||||
|
........................................................................ [ 26%]
|
||||||
|
........................................................................ [ 30%]
|
||||||
|
........................................................................ [ 35%]
|
||||||
|
........................................................................ [ 39%]
|
||||||
|
........................................................................ [ 44%]
|
||||||
|
........................................................................ [ 48%]
|
||||||
|
........................................................................ [ 53%]
|
||||||
|
........................................................................ [ 57%]
|
||||||
|
........................................................................ [ 61%]
|
||||||
|
........................................................................ [ 66%]
|
||||||
|
........................................................................ [ 70%]
|
||||||
|
........................................................................ [ 75%]
|
||||||
|
........................................................................ [ 79%]
|
||||||
|
........................................................................ [ 84%]
|
||||||
|
........................................................................ [ 88%]
|
||||||
|
........................................................................ [ 92%]
|
||||||
|
........................................................................ [ 97%]
|
||||||
|
........................................... [100%]
|
||||||
|
=============================== warnings summary ===============================
|
||||||
|
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
|
||||||
|
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
|
||||||
|
from starlette.testclient import TestClient as TestClient # noqa
|
||||||
|
|
||||||
|
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
|
||||||
|
================================ tests coverage ================================
|
||||||
|
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
|
||||||
|
|
||||||
|
Name Stmts Miss Cover
|
||||||
|
-----------------------------------------------
|
||||||
|
app/__init__.py 1 0 100%
|
||||||
|
app/api/__init__.py 0 0 100%
|
||||||
|
app/api/auth.py 40 0 100%
|
||||||
|
app/api/chat.py 178 0 100%
|
||||||
|
app/api/chats.py 110 0 100%
|
||||||
|
app/api/config.py 7 0 100%
|
||||||
|
app/api/doc_drafts.py 93 0 100%
|
||||||
|
app/api/docs.py 50 0 100%
|
||||||
|
app/api/git_sources.py 212 0 100%
|
||||||
|
app/api/health.py 10 0 100%
|
||||||
|
app/api/steering.py 42 0 100%
|
||||||
|
app/api/suggestions.py 11 0 100%
|
||||||
|
app/api/sync.py 101 0 100%
|
||||||
|
app/api/tokens.py 28 0 100%
|
||||||
|
app/config.py 140 0 100%
|
||||||
|
app/core/__init__.py 0 0 100%
|
||||||
|
app/core/auth.py 45 0 100%
|
||||||
|
app/core/caching.py 108 0 100%
|
||||||
|
app/core/debugging.py 29 2 93%
|
||||||
|
app/core/docs_push.py 39 0 100%
|
||||||
|
app/core/logging.py 13 0 100%
|
||||||
|
app/core/tokens.py 33 0 100%
|
||||||
|
app/db.py 21 0 100%
|
||||||
|
app/main.py 62 0 100%
|
||||||
|
app/models.py 94 0 100%
|
||||||
|
app/rag/__init__.py 0 0 100%
|
||||||
|
app/rag/agent.py 222 0 100%
|
||||||
|
app/rag/archive_upload.py 128 0 100%
|
||||||
|
app/rag/chunker.py 206 4 98%
|
||||||
|
app/rag/git_sources.py 14 0 100%
|
||||||
|
app/rag/importer.py 180 3 98%
|
||||||
|
app/rag/llm.py 216 0 100%
|
||||||
|
app/rag/overview.py 71 0 100%
|
||||||
|
app/rag/prompts.py 88 0 100%
|
||||||
|
app/rag/retriever.py 150 3 98%
|
||||||
|
app/rag/scaffolding.py 55 0 100%
|
||||||
|
app/rag/source_removal.py 41 0 100%
|
||||||
|
app/rag/sources_meta.py 16 0 100%
|
||||||
|
app/rag/suggestions.py 27 0 100%
|
||||||
|
app/rag/summarizer.py 24 0 100%
|
||||||
|
app/schemas.py 231 0 100%
|
||||||
|
-----------------------------------------------
|
||||||
|
TOTAL 3136 12 99%
|
||||||
|
coverage gate: app/ 99% (>90%) OK
|
||||||
|
All checks passed!
|
||||||
|
0 errors, 0 warnings, 0 informations
|
||||||
|
validation OK
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
All completion criteria verified green — no defects found, no fixes needed. Final report:
|
||||||
|
|
||||||
|
**Phase 80 (history_suggestion_chips) — final verification pass: ALL GREEN**
|
||||||
|
- Verified implementation: `last_questions()` + seed-fallback in `app/api/suggestions.py` (100% cov); `startNewChat()` refetch via `loadSuggestions()` in `app.js`; seed-semantics docs in `config.py` docstring, `.env.example:55`, README L114/L823; integration state matrix in `test_suggestions_api.py`; E2E suite rewritten to the 4 states + carried-over story behavior.
|
||||||
|
- `uv run pytest tests/e2e/test_suggestion_chips.py -v --no-cov` → **7 passed** (isolated).
|
||||||
|
- `uv run pytest --cov=app --cov-report=term-missing` → **1637 passed**, TOTAL **99%** (>90%).
|
||||||
|
- `uv run ruff check . && uv run pyright` → **All checks passed / 0 errors, 0 warnings**.
|
||||||
|
- Criterion: E2E isolated green → ✅.
|
||||||
|
- Criterion: deflection unchanged → ✅ (`app/rag/suggestions.py` no diff vs HEAD; `test_honest_deflection.py` 3 passed; 100% cov).
|
||||||
|
- Criterion: full suite + coverage + lint/types → ✅ (above).
|
||||||
|
- Criterion: atomic commit + phase dir move → deferred per harness override: all changes left uncommitted in the working tree; task files already in `complete/80_history_suggestion_chips/`; harness commits and moves the phase dir.
|
||||||
|
- Notable: none — no defects found in prior work.
|
||||||
|
- Next pending phase: **none** (phase 80 is the last in the roadmap; `todo/` contains only this phase).
|
||||||
+81
@@ -0,0 +1,81 @@
|
|||||||
|
........................................................................ [ 4%]
|
||||||
|
........................................................................ [ 8%]
|
||||||
|
........................................................................ [ 13%]
|
||||||
|
........................................................................ [ 17%]
|
||||||
|
........................................................................ [ 21%]
|
||||||
|
........................................................................ [ 26%]
|
||||||
|
........................................................................ [ 30%]
|
||||||
|
........................................................................ [ 35%]
|
||||||
|
........................................................................ [ 39%]
|
||||||
|
........................................................................ [ 43%]
|
||||||
|
........................................................................ [ 48%]
|
||||||
|
........................................................................ [ 52%]
|
||||||
|
........................................................................ [ 57%]
|
||||||
|
........................................................................ [ 61%]
|
||||||
|
........................................................................ [ 65%]
|
||||||
|
........................................................................ [ 70%]
|
||||||
|
........................................................................ [ 74%]
|
||||||
|
........................................................................ [ 79%]
|
||||||
|
........................................................................ [ 83%]
|
||||||
|
........................................................................ [ 87%]
|
||||||
|
........................................................................ [ 92%]
|
||||||
|
........................................................................ [ 96%]
|
||||||
|
..................................................... [100%]
|
||||||
|
=============================== warnings summary ===============================
|
||||||
|
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
|
||||||
|
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
|
||||||
|
from starlette.testclient import TestClient as TestClient # noqa
|
||||||
|
|
||||||
|
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
|
||||||
|
================================ tests coverage ================================
|
||||||
|
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
|
||||||
|
|
||||||
|
Name Stmts Miss Cover
|
||||||
|
-----------------------------------------------
|
||||||
|
app/__init__.py 1 0 100%
|
||||||
|
app/api/__init__.py 0 0 100%
|
||||||
|
app/api/auth.py 40 0 100%
|
||||||
|
app/api/chat.py 178 0 100%
|
||||||
|
app/api/chats.py 110 0 100%
|
||||||
|
app/api/config.py 7 0 100%
|
||||||
|
app/api/doc_drafts.py 93 0 100%
|
||||||
|
app/api/docs.py 50 0 100%
|
||||||
|
app/api/git_sources.py 212 0 100%
|
||||||
|
app/api/health.py 10 0 100%
|
||||||
|
app/api/steering.py 42 0 100%
|
||||||
|
app/api/suggestions.py 29 0 100%
|
||||||
|
app/api/sync.py 101 0 100%
|
||||||
|
app/api/tokens.py 28 0 100%
|
||||||
|
app/config.py 140 0 100%
|
||||||
|
app/core/__init__.py 0 0 100%
|
||||||
|
app/core/auth.py 45 0 100%
|
||||||
|
app/core/caching.py 108 0 100%
|
||||||
|
app/core/debugging.py 29 2 93%
|
||||||
|
app/core/docs_push.py 39 0 100%
|
||||||
|
app/core/logging.py 13 0 100%
|
||||||
|
app/core/tokens.py 33 0 100%
|
||||||
|
app/db.py 21 0 100%
|
||||||
|
app/main.py 62 0 100%
|
||||||
|
app/models.py 94 0 100%
|
||||||
|
app/rag/__init__.py 0 0 100%
|
||||||
|
app/rag/agent.py 222 0 100%
|
||||||
|
app/rag/archive_upload.py 128 0 100%
|
||||||
|
app/rag/chunker.py 206 4 98%
|
||||||
|
app/rag/git_sources.py 14 0 100%
|
||||||
|
app/rag/importer.py 180 3 98%
|
||||||
|
app/rag/llm.py 216 0 100%
|
||||||
|
app/rag/overview.py 71 0 100%
|
||||||
|
app/rag/prompts.py 88 0 100%
|
||||||
|
app/rag/retriever.py 150 3 98%
|
||||||
|
app/rag/scaffolding.py 55 0 100%
|
||||||
|
app/rag/source_removal.py 41 0 100%
|
||||||
|
app/rag/sources_meta.py 16 0 100%
|
||||||
|
app/rag/suggestions.py 27 0 100%
|
||||||
|
app/rag/summarizer.py 24 0 100%
|
||||||
|
app/schemas.py 231 0 100%
|
||||||
|
-----------------------------------------------
|
||||||
|
TOTAL 3154 12 99%
|
||||||
|
coverage gate: app/ 99% (>90%) OK
|
||||||
|
All checks passed!
|
||||||
|
0 errors, 0 warnings, 0 informations
|
||||||
|
validation OK
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
**Task 01 complete** — `/api/suggestions` now returns the last 3 questions asked, seed before the first.
|
||||||
|
|
||||||
|
- `app/api/suggestions.py`: module-level `last_questions(db, limit=3)` — `saved_chats` walked `updated_at DESC, created_at DESC`, each `bor.chat.v1` message list walked in reverse, `who=="user"` non-blank trimmed texts, exact case-sensitive dedup, cap 3; endpoint returns `qs if qs else get_settings().suggestions` (phase-79 `require_user` gate kept, `db` dep now used).
|
||||||
|
- New `tests/integration/test_suggestions_api.py` (10 tests): empty DB → seed; 4-q chat → `[q4,q3,q2]`; distinct-`updated_at` chat order; verbatim cross-chat dedup; case-sensitive dedup; exactly-2 and exactly-1 chips (no seed top-up); brain/blank-skip + all-brain → seed; anonymous → 401.
|
||||||
|
- Fixed stale pre-change pins in `tests/integration/test_api.py` (shape pin no longer assumes ≥3; env-override pin now conditioned on empty `saved_chats`, documented as the module's DB exception).
|
||||||
|
- `app/rag/suggestions.py` (deflection "Maybe try") untouched; its suites green.
|
||||||
|
|
||||||
|
Results: `uv run pytest --cov=app --cov-report=term` → 1637 passed, app/ coverage 99% (suggestions.py 100%); `uv run ruff check .` clean; `uv run pyright` → 0 errors.
|
||||||
|
|
||||||
|
No deviations. Dev DB left as found (saved_chats empty). Next pending task: `02_seed_semantics_docs.md`.
|
||||||
+81
@@ -0,0 +1,81 @@
|
|||||||
|
........................................................................ [ 4%]
|
||||||
|
........................................................................ [ 8%]
|
||||||
|
........................................................................ [ 13%]
|
||||||
|
........................................................................ [ 17%]
|
||||||
|
........................................................................ [ 21%]
|
||||||
|
........................................................................ [ 26%]
|
||||||
|
........................................................................ [ 30%]
|
||||||
|
........................................................................ [ 35%]
|
||||||
|
........................................................................ [ 39%]
|
||||||
|
........................................................................ [ 43%]
|
||||||
|
........................................................................ [ 48%]
|
||||||
|
........................................................................ [ 52%]
|
||||||
|
........................................................................ [ 57%]
|
||||||
|
........................................................................ [ 61%]
|
||||||
|
........................................................................ [ 65%]
|
||||||
|
........................................................................ [ 70%]
|
||||||
|
........................................................................ [ 74%]
|
||||||
|
........................................................................ [ 79%]
|
||||||
|
........................................................................ [ 83%]
|
||||||
|
........................................................................ [ 87%]
|
||||||
|
........................................................................ [ 92%]
|
||||||
|
........................................................................ [ 96%]
|
||||||
|
..................................................... [100%]
|
||||||
|
=============================== warnings summary ===============================
|
||||||
|
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
|
||||||
|
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
|
||||||
|
from starlette.testclient import TestClient as TestClient # noqa
|
||||||
|
|
||||||
|
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
|
||||||
|
================================ tests coverage ================================
|
||||||
|
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
|
||||||
|
|
||||||
|
Name Stmts Miss Cover
|
||||||
|
-----------------------------------------------
|
||||||
|
app/__init__.py 1 0 100%
|
||||||
|
app/api/__init__.py 0 0 100%
|
||||||
|
app/api/auth.py 40 0 100%
|
||||||
|
app/api/chat.py 178 0 100%
|
||||||
|
app/api/chats.py 110 0 100%
|
||||||
|
app/api/config.py 7 0 100%
|
||||||
|
app/api/doc_drafts.py 93 0 100%
|
||||||
|
app/api/docs.py 50 0 100%
|
||||||
|
app/api/git_sources.py 212 0 100%
|
||||||
|
app/api/health.py 10 0 100%
|
||||||
|
app/api/steering.py 42 0 100%
|
||||||
|
app/api/suggestions.py 29 0 100%
|
||||||
|
app/api/sync.py 101 0 100%
|
||||||
|
app/api/tokens.py 28 0 100%
|
||||||
|
app/config.py 140 0 100%
|
||||||
|
app/core/__init__.py 0 0 100%
|
||||||
|
app/core/auth.py 45 0 100%
|
||||||
|
app/core/caching.py 108 0 100%
|
||||||
|
app/core/debugging.py 29 2 93%
|
||||||
|
app/core/docs_push.py 39 0 100%
|
||||||
|
app/core/logging.py 13 0 100%
|
||||||
|
app/core/tokens.py 33 0 100%
|
||||||
|
app/db.py 21 0 100%
|
||||||
|
app/main.py 62 0 100%
|
||||||
|
app/models.py 94 0 100%
|
||||||
|
app/rag/__init__.py 0 0 100%
|
||||||
|
app/rag/agent.py 222 0 100%
|
||||||
|
app/rag/archive_upload.py 128 0 100%
|
||||||
|
app/rag/chunker.py 206 4 98%
|
||||||
|
app/rag/git_sources.py 14 0 100%
|
||||||
|
app/rag/importer.py 180 3 98%
|
||||||
|
app/rag/llm.py 216 0 100%
|
||||||
|
app/rag/overview.py 71 0 100%
|
||||||
|
app/rag/prompts.py 88 0 100%
|
||||||
|
app/rag/retriever.py 150 3 98%
|
||||||
|
app/rag/scaffolding.py 55 0 100%
|
||||||
|
app/rag/source_removal.py 41 0 100%
|
||||||
|
app/rag/sources_meta.py 16 0 100%
|
||||||
|
app/rag/suggestions.py 27 0 100%
|
||||||
|
app/rag/summarizer.py 24 0 100%
|
||||||
|
app/schemas.py 231 0 100%
|
||||||
|
-----------------------------------------------
|
||||||
|
TOTAL 3154 12 99%
|
||||||
|
coverage gate: app/ 99% (>90%) OK
|
||||||
|
All checks passed!
|
||||||
|
0 errors, 0 warnings, 0 informations
|
||||||
|
validation OK
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
Task 02 complete. All four doc sites updated (docs-only, no behavior change):
|
||||||
|
|
||||||
|
- **`app/config.py`** — comment block above `suggestions: list[str]` now documents the SEED semantics: shown only while no saved chat has ever asked a question; afterwards `GET /api/suggestions` serves the last 3 questions (deployment-wide, newest first); `BOR_SUGGESTIONS` overrides the seed. Default list untouched.
|
||||||
|
- **`.env.example:55`** — comment now reads "JSON seed chips — shown only before any question has been saved (phase 80)".
|
||||||
|
- **`README.md` env-table row** — "JSON seed for the onboarding chips — shown only before the first saved question; afterwards the chips are the last 3 questions asked (phase 80)".
|
||||||
|
- **`README.md` chat feature description** — one sentence added: onboarding chips follow the last 3 questions asked, seeding from `BOR_SUGGESTIONS` on a fresh deployment. Deflection note and access-table row left as-is (unchanged contracts).
|
||||||
|
|
||||||
|
**Verification:**
|
||||||
|
- `uv run pytest --cov=app --cov-report=term-missing` → 1637 passed, `app/` coverage **99%** (>90% gate)
|
||||||
|
- `uv run ruff check .` → All checks passed
|
||||||
|
- `uv run pyright` → 0 errors, 0 warnings
|
||||||
|
|
||||||
|
**Notable:** no defects found; deflection contract (`app/rag/suggestions.py` + suites) green and untouched. The e2e story-suite rewrite for the new chip semantics remains deferred to task 04 per the phase plan (phase-76 precedent), so the old-contract e2e pins are expected to go until then.
|
||||||
|
|
||||||
|
**Next pending task:** `03_chips_new_chat_refetch.md` (app.js refetch on New chat + source pin).
|
||||||
+81
@@ -0,0 +1,81 @@
|
|||||||
|
........................................................................ [ 4%]
|
||||||
|
........................................................................ [ 8%]
|
||||||
|
........................................................................ [ 13%]
|
||||||
|
........................................................................ [ 17%]
|
||||||
|
........................................................................ [ 21%]
|
||||||
|
........................................................................ [ 26%]
|
||||||
|
........................................................................ [ 30%]
|
||||||
|
........................................................................ [ 35%]
|
||||||
|
........................................................................ [ 39%]
|
||||||
|
........................................................................ [ 43%]
|
||||||
|
........................................................................ [ 48%]
|
||||||
|
........................................................................ [ 52%]
|
||||||
|
........................................................................ [ 57%]
|
||||||
|
........................................................................ [ 61%]
|
||||||
|
........................................................................ [ 65%]
|
||||||
|
........................................................................ [ 70%]
|
||||||
|
........................................................................ [ 74%]
|
||||||
|
........................................................................ [ 79%]
|
||||||
|
........................................................................ [ 83%]
|
||||||
|
........................................................................ [ 87%]
|
||||||
|
........................................................................ [ 92%]
|
||||||
|
........................................................................ [ 96%]
|
||||||
|
..................................................... [100%]
|
||||||
|
=============================== warnings summary ===============================
|
||||||
|
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
|
||||||
|
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
|
||||||
|
from starlette.testclient import TestClient as TestClient # noqa
|
||||||
|
|
||||||
|
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
|
||||||
|
================================ tests coverage ================================
|
||||||
|
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
|
||||||
|
|
||||||
|
Name Stmts Miss Cover
|
||||||
|
-----------------------------------------------
|
||||||
|
app/__init__.py 1 0 100%
|
||||||
|
app/api/__init__.py 0 0 100%
|
||||||
|
app/api/auth.py 40 0 100%
|
||||||
|
app/api/chat.py 178 0 100%
|
||||||
|
app/api/chats.py 110 0 100%
|
||||||
|
app/api/config.py 7 0 100%
|
||||||
|
app/api/doc_drafts.py 93 0 100%
|
||||||
|
app/api/docs.py 50 0 100%
|
||||||
|
app/api/git_sources.py 212 0 100%
|
||||||
|
app/api/health.py 10 0 100%
|
||||||
|
app/api/steering.py 42 0 100%
|
||||||
|
app/api/suggestions.py 29 0 100%
|
||||||
|
app/api/sync.py 101 0 100%
|
||||||
|
app/api/tokens.py 28 0 100%
|
||||||
|
app/config.py 140 0 100%
|
||||||
|
app/core/__init__.py 0 0 100%
|
||||||
|
app/core/auth.py 45 0 100%
|
||||||
|
app/core/caching.py 108 0 100%
|
||||||
|
app/core/debugging.py 29 2 93%
|
||||||
|
app/core/docs_push.py 39 0 100%
|
||||||
|
app/core/logging.py 13 0 100%
|
||||||
|
app/core/tokens.py 33 0 100%
|
||||||
|
app/db.py 21 0 100%
|
||||||
|
app/main.py 62 0 100%
|
||||||
|
app/models.py 94 0 100%
|
||||||
|
app/rag/__init__.py 0 0 100%
|
||||||
|
app/rag/agent.py 222 0 100%
|
||||||
|
app/rag/archive_upload.py 128 0 100%
|
||||||
|
app/rag/chunker.py 206 4 98%
|
||||||
|
app/rag/git_sources.py 14 0 100%
|
||||||
|
app/rag/importer.py 180 3 98%
|
||||||
|
app/rag/llm.py 216 0 100%
|
||||||
|
app/rag/overview.py 71 0 100%
|
||||||
|
app/rag/prompts.py 88 0 100%
|
||||||
|
app/rag/retriever.py 150 3 98%
|
||||||
|
app/rag/scaffolding.py 55 0 100%
|
||||||
|
app/rag/source_removal.py 41 0 100%
|
||||||
|
app/rag/sources_meta.py 16 0 100%
|
||||||
|
app/rag/suggestions.py 27 0 100%
|
||||||
|
app/rag/summarizer.py 24 0 100%
|
||||||
|
app/schemas.py 231 0 100%
|
||||||
|
-----------------------------------------------
|
||||||
|
TOTAL 3154 12 99%
|
||||||
|
coverage gate: app/ 99% (>90%) OK
|
||||||
|
All checks passed!
|
||||||
|
0 errors, 0 warnings, 0 informations
|
||||||
|
validation OK
|
||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
Task 03 complete — all gates green.
|
||||||
|
|
||||||
|
**Implemented**
|
||||||
|
- `frontend/assets/app.js`: `startNewChat()` (the `bor:new-chat` handler) now calls `loadSuggestions()` right after the empty state is restored — the onboarding chips refetch `/api/suggestions` so the row reflects the current last-3 state, never the stale boot fetch. Progressive enhancement unchanged (swallows 401/network failures); boot path untouched; in-flight-turn guard still exempts a live turn.
|
||||||
|
- `tests/unit/test_chat_persistence.py`: source pin in `test_new_chat_clears_key_and_ui` — containment assertion that `loadSuggestions()` appears in the `startNewChat` function-body slice (house style, two literals).
|
||||||
|
|
||||||
|
**Test / lint / coverage**
|
||||||
|
- `node --check frontend/assets/app.js` — valid JS
|
||||||
|
- `uv run pytest -q` (unit + integration) — exit 0, all passed
|
||||||
|
- `uv run pytest --cov=app --cov-report=term-missing` — TOTAL **99%** (>90% floor)
|
||||||
|
- `uv run ruff check .` — All checks passed; `uv run pyright` — 0 errors
|
||||||
|
- `uv run pytest tests/unit/test_chat_persistence.py::test_new_chat_clears_key_and_ui` — passed
|
||||||
|
|
||||||
|
**Decisions / notes**
|
||||||
|
- Pin placed in `test_chat_persistence.py` (the existing `startNewChat` body-slice pin — the file the task names first).
|
||||||
|
- `.agents/PLAN.md` is absent from the repo; the phase files carry the binding decisions (no deviation).
|
||||||
|
- E2E refetch assertion belongs to task 04's suite (per task file); no other files touched. My diff is only the 2 files above (working tree also carries uncommitted harness work from phases 77–79, left as-is).
|
||||||
|
|
||||||
|
**Next pending task:** `.agents/phases/todo/80_history_suggestion_chips/04_e2e_suite_commit.md`
|
||||||
+215
@@ -0,0 +1,215 @@
|
|||||||
|
........................................................................ [ 4%]
|
||||||
|
........................................................................ [ 8%]
|
||||||
|
........................................................................ [ 13%]
|
||||||
|
........................................................................ [ 17%]
|
||||||
|
........................................................................ [ 21%]
|
||||||
|
........................................................................ [ 26%]
|
||||||
|
........................................................................ [ 30%]
|
||||||
|
........................................................................ [ 35%]
|
||||||
|
........................................................................ [ 39%]
|
||||||
|
........................................................................ [ 43%]
|
||||||
|
........................................................................ [ 48%]
|
||||||
|
........................................................................ [ 52%]
|
||||||
|
........................................................................ [ 57%]
|
||||||
|
........................................................................ [ 61%]
|
||||||
|
........................................................................ [ 65%]
|
||||||
|
........................................................................ [ 70%]
|
||||||
|
........................................................................ [ 74%]
|
||||||
|
........................................................................ [ 79%]
|
||||||
|
........................................................................ [ 83%]
|
||||||
|
........................................................................ [ 87%]
|
||||||
|
........................................................................ [ 92%]
|
||||||
|
........................................................................ [ 96%]
|
||||||
|
..................................................... [100%]
|
||||||
|
=============================== warnings summary ===============================
|
||||||
|
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
|
||||||
|
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
|
||||||
|
from starlette.testclient import TestClient as TestClient # noqa
|
||||||
|
|
||||||
|
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
|
||||||
|
================================ tests coverage ================================
|
||||||
|
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
|
||||||
|
|
||||||
|
Name Stmts Miss Cover
|
||||||
|
-----------------------------------------------
|
||||||
|
app/__init__.py 1 0 100%
|
||||||
|
app/api/__init__.py 0 0 100%
|
||||||
|
app/api/auth.py 40 0 100%
|
||||||
|
app/api/chat.py 178 0 100%
|
||||||
|
app/api/chats.py 110 0 100%
|
||||||
|
app/api/config.py 7 0 100%
|
||||||
|
app/api/doc_drafts.py 93 0 100%
|
||||||
|
app/api/docs.py 50 0 100%
|
||||||
|
app/api/git_sources.py 212 0 100%
|
||||||
|
app/api/health.py 10 0 100%
|
||||||
|
app/api/steering.py 42 0 100%
|
||||||
|
app/api/suggestions.py 29 0 100%
|
||||||
|
app/api/sync.py 101 0 100%
|
||||||
|
app/api/tokens.py 28 0 100%
|
||||||
|
app/config.py 140 0 100%
|
||||||
|
app/core/__init__.py 0 0 100%
|
||||||
|
app/core/auth.py 45 0 100%
|
||||||
|
app/core/caching.py 108 0 100%
|
||||||
|
app/core/debugging.py 29 2 93%
|
||||||
|
app/core/docs_push.py 39 0 100%
|
||||||
|
app/core/logging.py 13 0 100%
|
||||||
|
app/core/tokens.py 33 0 100%
|
||||||
|
app/db.py 21 0 100%
|
||||||
|
app/main.py 62 0 100%
|
||||||
|
app/models.py 94 0 100%
|
||||||
|
app/rag/__init__.py 0 0 100%
|
||||||
|
app/rag/agent.py 222 0 100%
|
||||||
|
app/rag/archive_upload.py 128 0 100%
|
||||||
|
app/rag/chunker.py 206 4 98%
|
||||||
|
app/rag/git_sources.py 14 0 100%
|
||||||
|
app/rag/importer.py 180 3 98%
|
||||||
|
app/rag/llm.py 216 0 100%
|
||||||
|
app/rag/overview.py 71 0 100%
|
||||||
|
app/rag/prompts.py 88 0 100%
|
||||||
|
app/rag/retriever.py 150 3 98%
|
||||||
|
app/rag/scaffolding.py 55 0 100%
|
||||||
|
app/rag/source_removal.py 41 0 100%
|
||||||
|
app/rag/sources_meta.py 16 0 100%
|
||||||
|
app/rag/suggestions.py 27 0 100%
|
||||||
|
app/rag/summarizer.py 24 0 100%
|
||||||
|
app/schemas.py 231 0 100%
|
||||||
|
-----------------------------------------------
|
||||||
|
TOTAL 3154 12 99%
|
||||||
|
coverage gate: app/ 99% (>90%) OK
|
||||||
|
I001 [*] Import block is un-sorted or un-formatted
|
||||||
|
--> tests/e2e/test_toggle_after_nav.py:2:1
|
||||||
|
|
|
||||||
|
1 | """Quick diagnostic: does the toggle still work after SPA navigation?"""
|
||||||
|
2 | / import pytest
|
||||||
|
3 | | from playwright.sync_api import Browser, expect
|
||||||
|
| |_______________________________________________^
|
||||||
|
4 |
|
||||||
|
5 | def test_toggle_after_spa_nav(browser, app_url, db_ready):
|
||||||
|
|
|
||||||
|
help: Organize imports
|
||||||
|
|
|
||||||
|
4 |
|
||||||
|
5 +
|
||||||
|
6 | def test_toggle_after_spa_nav(browser, app_url, db_ready):
|
||||||
|
|
|
||||||
|
|
||||||
|
F401 [*] `pytest` imported but unused
|
||||||
|
--> tests/e2e/test_toggle_after_nav.py:2:8
|
||||||
|
|
|
||||||
|
1 | """Quick diagnostic: does the toggle still work after SPA navigation?"""
|
||||||
|
2 | import pytest
|
||||||
|
| ^^^^^^
|
||||||
|
3 | from playwright.sync_api import Browser, expect
|
||||||
|
|
|
||||||
|
help: Remove unused import: `pytest`
|
||||||
|
|
|
||||||
|
1 | """Quick diagnostic: does the toggle still work after SPA navigation?"""
|
||||||
|
- import pytest
|
||||||
|
2 | from playwright.sync_api import Browser, expect
|
||||||
|
|
|
||||||
|
|
||||||
|
F401 [*] `playwright.sync_api.Browser` imported but unused
|
||||||
|
--> tests/e2e/test_toggle_after_nav.py:3:33
|
||||||
|
|
|
||||||
|
1 | """Quick diagnostic: does the toggle still work after SPA navigation?"""
|
||||||
|
2 | import pytest
|
||||||
|
3 | from playwright.sync_api import Browser, expect
|
||||||
|
| ^^^^^^^
|
||||||
|
4 |
|
||||||
|
5 | def test_toggle_after_spa_nav(browser, app_url, db_ready):
|
||||||
|
|
|
||||||
|
help: Remove unused import: `playwright.sync_api.Browser`
|
||||||
|
|
|
||||||
|
2 | import pytest
|
||||||
|
- from playwright.sync_api import Browser, expect
|
||||||
|
3 + from playwright.sync_api import expect
|
||||||
|
4 |
|
||||||
|
|
|
||||||
|
|
||||||
|
W293 [*] Blank line contains whitespace
|
||||||
|
--> tests/e2e/test_toggle_after_nav.py:15:1
|
||||||
|
|
|
||||||
|
13 | timeout=10_000,
|
||||||
|
14 | )
|
||||||
|
15 |
|
||||||
|
| ^^^^^^^^
|
||||||
|
16 | # Open the menu
|
||||||
|
17 | page.click("#nav-toggle")
|
||||||
|
|
|
||||||
|
help: Remove whitespace from blank line
|
||||||
|
|
|
||||||
|
14 | )
|
||||||
|
-
|
||||||
|
15 +
|
||||||
|
16 | # Open the menu
|
||||||
|
|
|
||||||
|
|
||||||
|
W293 [*] Blank line contains whitespace
|
||||||
|
--> tests/e2e/test_toggle_after_nav.py:19:1
|
||||||
|
|
|
||||||
|
17 | page.click("#nav-toggle")
|
||||||
|
18 | expect(page.locator("#nav-toggle")).to_have_attribute("aria-expanded", "true")
|
||||||
|
19 |
|
||||||
|
| ^^^^^^^^
|
||||||
|
20 | # Navigate to a different view via SPA
|
||||||
|
21 | page.click("#nav-sources")
|
||||||
|
|
|
||||||
|
help: Remove whitespace from blank line
|
||||||
|
|
|
||||||
|
18 | expect(page.locator("#nav-toggle")).to_have_attribute("aria-expanded", "true")
|
||||||
|
-
|
||||||
|
19 +
|
||||||
|
20 | # Navigate to a different view via SPA
|
||||||
|
|
|
||||||
|
|
||||||
|
W293 [*] Blank line contains whitespace
|
||||||
|
--> tests/e2e/test_toggle_after_nav.py:23:1
|
||||||
|
|
|
||||||
|
21 | page.click("#nav-sources")
|
||||||
|
22 | expect(page).to_have_url(app_url + "/sources.html", timeout=15_000)
|
||||||
|
23 |
|
||||||
|
| ^^^^^^^^
|
||||||
|
24 | # Menu should be closed
|
||||||
|
25 | expect(page.locator("#nav-toggle")).to_have_attribute("aria-expanded", "false")
|
||||||
|
|
|
||||||
|
help: Remove whitespace from blank line
|
||||||
|
|
|
||||||
|
22 | expect(page).to_have_url(app_url + "/sources.html", timeout=15_000)
|
||||||
|
-
|
||||||
|
23 +
|
||||||
|
24 | # Menu should be closed
|
||||||
|
|
|
||||||
|
|
||||||
|
W293 [*] Blank line contains whitespace
|
||||||
|
--> tests/e2e/test_toggle_after_nav.py:26:1
|
||||||
|
|
|
||||||
|
24 | # Menu should be closed
|
||||||
|
25 | expect(page.locator("#nav-toggle")).to_have_attribute("aria-expanded", "false")
|
||||||
|
26 |
|
||||||
|
| ^^^^^^^^
|
||||||
|
27 | # NOW try to open the menu again - THIS IS THE BUG CHECK
|
||||||
|
28 | page.click("#nav-toggle")
|
||||||
|
|
|
||||||
|
help: Remove whitespace from blank line
|
||||||
|
|
|
||||||
|
25 | expect(page.locator("#nav-toggle")).to_have_attribute("aria-expanded", "false")
|
||||||
|
-
|
||||||
|
26 +
|
||||||
|
27 | # NOW try to open the menu again - THIS IS THE BUG CHECK
|
||||||
|
|
|
||||||
|
|
||||||
|
E501 Line too long (104 > 100)
|
||||||
|
--> tests/e2e/test_toggle_after_nav.py:30:101
|
||||||
|
|
|
||||||
|
28 | page.click("#nav-toggle")
|
||||||
|
29 | try:
|
||||||
|
30 | expect(page.locator("#nav-toggle")).to_have_attribute("aria-expanded", "true", timeout=3000)
|
||||||
|
| ^^^^
|
||||||
|
31 | print("PASS: Toggle still works after SPA navigation")
|
||||||
|
32 | except Exception as e:
|
||||||
|
|
|
||||||
|
|
||||||
|
Found 8 errors.
|
||||||
|
[*] 7 fixable with the `--fix` option.
|
||||||
|
0 errors, 0 warnings, 0 informations
|
||||||
|
validation FAILED (see output above)
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
Everything is green — the only "error" match is pyright's "0 errors" line.
|
||||||
|
|
||||||
|
**Fixed**
|
||||||
|
- `tests/e2e/test_toggle_after_nav.py` (stray diagnostic file, pre-existing lint debt that failed the harness gate): removed unused imports (`pytest`, `Browser`), fixed the isort order (third-party `playwright` block, then first-party `e2e.auth_helpers` — house style), stripped whitespace-only lines, wrapped the >100-char `expect(...)` line. Preserved the file's current semantics (phase-79 `login(page, app_url, next="/")` admin wait — the variant present on disk).
|
||||||
|
|
||||||
|
**Test / lint / coverage**
|
||||||
|
- `bash .agents/validate.sh` (the harness gate) — **exit 0, "validation OK"**
|
||||||
|
- Inside it: `uv run pytest --cov=app` — all unit + integration passed; TOTAL **99%** (>90% floor)
|
||||||
|
- `uv run ruff check .` — All checks passed; `uv run pyright` — 0 errors
|
||||||
|
|
||||||
|
**Decisions / notes**
|
||||||
|
- The diagnostic file was being updated concurrently (a parallel process overwrote my first fix with the `login`-helper variant); I lint-cleaned that on-disk variant instead and re-validated immediately — final state verified green.
|
||||||
|
- Task-03 work (app.js `loadSuggestions()` in `startNewChat()` + the source pin in `test_chat_persistence.py`) remains in the tree, untouched and green.
|
||||||
|
|
||||||
|
**Next pending task:** `.agents/phases/todo/80_history_suggestion_chips/04_e2e_suite_commit.md`
|
||||||
+81
@@ -0,0 +1,81 @@
|
|||||||
|
........................................................................ [ 4%]
|
||||||
|
........................................................................ [ 8%]
|
||||||
|
........................................................................ [ 13%]
|
||||||
|
........................................................................ [ 17%]
|
||||||
|
........................................................................ [ 21%]
|
||||||
|
........................................................................ [ 26%]
|
||||||
|
........................................................................ [ 30%]
|
||||||
|
........................................................................ [ 35%]
|
||||||
|
........................................................................ [ 39%]
|
||||||
|
........................................................................ [ 43%]
|
||||||
|
........................................................................ [ 48%]
|
||||||
|
........................................................................ [ 52%]
|
||||||
|
........................................................................ [ 57%]
|
||||||
|
........................................................................ [ 61%]
|
||||||
|
........................................................................ [ 65%]
|
||||||
|
........................................................................ [ 70%]
|
||||||
|
........................................................................ [ 74%]
|
||||||
|
........................................................................ [ 79%]
|
||||||
|
........................................................................ [ 83%]
|
||||||
|
........................................................................ [ 87%]
|
||||||
|
........................................................................ [ 92%]
|
||||||
|
........................................................................ [ 96%]
|
||||||
|
..................................................... [100%]
|
||||||
|
=============================== warnings summary ===============================
|
||||||
|
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
|
||||||
|
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
|
||||||
|
from starlette.testclient import TestClient as TestClient # noqa
|
||||||
|
|
||||||
|
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
|
||||||
|
================================ tests coverage ================================
|
||||||
|
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
|
||||||
|
|
||||||
|
Name Stmts Miss Cover
|
||||||
|
-----------------------------------------------
|
||||||
|
app/__init__.py 1 0 100%
|
||||||
|
app/api/__init__.py 0 0 100%
|
||||||
|
app/api/auth.py 40 0 100%
|
||||||
|
app/api/chat.py 178 0 100%
|
||||||
|
app/api/chats.py 110 0 100%
|
||||||
|
app/api/config.py 7 0 100%
|
||||||
|
app/api/doc_drafts.py 93 0 100%
|
||||||
|
app/api/docs.py 50 0 100%
|
||||||
|
app/api/git_sources.py 212 0 100%
|
||||||
|
app/api/health.py 10 0 100%
|
||||||
|
app/api/steering.py 42 0 100%
|
||||||
|
app/api/suggestions.py 29 0 100%
|
||||||
|
app/api/sync.py 101 0 100%
|
||||||
|
app/api/tokens.py 28 0 100%
|
||||||
|
app/config.py 140 0 100%
|
||||||
|
app/core/__init__.py 0 0 100%
|
||||||
|
app/core/auth.py 45 0 100%
|
||||||
|
app/core/caching.py 108 0 100%
|
||||||
|
app/core/debugging.py 29 2 93%
|
||||||
|
app/core/docs_push.py 39 0 100%
|
||||||
|
app/core/logging.py 13 0 100%
|
||||||
|
app/core/tokens.py 33 0 100%
|
||||||
|
app/db.py 21 0 100%
|
||||||
|
app/main.py 62 0 100%
|
||||||
|
app/models.py 94 0 100%
|
||||||
|
app/rag/__init__.py 0 0 100%
|
||||||
|
app/rag/agent.py 222 0 100%
|
||||||
|
app/rag/archive_upload.py 128 0 100%
|
||||||
|
app/rag/chunker.py 206 4 98%
|
||||||
|
app/rag/git_sources.py 14 0 100%
|
||||||
|
app/rag/importer.py 180 3 98%
|
||||||
|
app/rag/llm.py 216 0 100%
|
||||||
|
app/rag/overview.py 71 0 100%
|
||||||
|
app/rag/prompts.py 88 0 100%
|
||||||
|
app/rag/retriever.py 150 3 98%
|
||||||
|
app/rag/scaffolding.py 55 0 100%
|
||||||
|
app/rag/source_removal.py 41 0 100%
|
||||||
|
app/rag/sources_meta.py 16 0 100%
|
||||||
|
app/rag/suggestions.py 27 0 100%
|
||||||
|
app/rag/summarizer.py 24 0 100%
|
||||||
|
app/schemas.py 231 0 100%
|
||||||
|
-----------------------------------------------
|
||||||
|
TOTAL 3154 12 99%
|
||||||
|
coverage gate: app/ 99% (>90%) OK
|
||||||
|
All checks passed!
|
||||||
|
0 errors, 0 warnings, 0 informations
|
||||||
|
validation OK
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
Task 04 complete — all gates green. Report:
|
||||||
|
|
||||||
|
**Implemented**
|
||||||
|
- Rewrote `tests/e2e/test_suggestion_chips.py` (7 tests) to the phase-80 contract: **seed** state (chips == exact `SEED` literal copied from the `Settings.suggestions` default, role=list/buttons/style pins), **last-3** (2 chats via `POST /api/chats`, 5 questions, older saved first → fresh load shows exactly the 3 newest, newest-first), **partial** (2 saved questions → exactly 2 chips, no seed top-up), **new-chat refetch** (click `#new-chat-btn` → chips become Q + a second `GET /api/suggestions` in the request log, timestamped after the click) — plus carried-over one-tap submit, Tab+Enter keyboard walk, and the mobile single-scroll-row contract.
|
||||||
|
- Added an autouse `clean_chats` fixture truncating `saved_chats` before/after every test — this also fixed pre-existing defects I found: the old suite (unmodified by prior tasks) was already failing 2/4 because auto-saved chat rows leaked between tests and the new DB-derived contract turned them into 1 deduped chip.
|
||||||
|
|
||||||
|
**Results**
|
||||||
|
- `uv run pytest tests/e2e/test_suggestion_chips.py -v --no-cov` → 7 passed (green in isolation, repeated runs stable)
|
||||||
|
- `uv run pytest --cov=app --cov-report=term-missing` → 1637 passed, **99%** on `app/` (`app/api/suggestions.py` 100%)
|
||||||
|
- `uv run ruff check .` → clean; `uv run pyright` → 0 errors
|
||||||
|
- Deflection untouched: `app/rag/suggestions.py` has no diff; `uv run pytest tests/e2e/test_honest_deflection.py --no-cov` → 3 passed
|
||||||
|
|
||||||
|
**Notes**
|
||||||
|
- No `git add`/`commit` and no phase-dir move performed — the harness owns the atomic commit and the move (overrides the task's commit step).
|
||||||
|
- Playwright's `APIRequestContext.post` in this version has no `json=` kwarg — JSON is sent via `data=` + `Content-Type: application/json`.
|
||||||
|
|
||||||
|
**Next pending task:** none — task 04 was the final task; phase 80 is complete, awaiting the harness commit.
|
||||||
+81
@@ -0,0 +1,81 @@
|
|||||||
|
........................................................................ [ 4%]
|
||||||
|
........................................................................ [ 8%]
|
||||||
|
........................................................................ [ 13%]
|
||||||
|
........................................................................ [ 17%]
|
||||||
|
........................................................................ [ 21%]
|
||||||
|
........................................................................ [ 26%]
|
||||||
|
........................................................................ [ 30%]
|
||||||
|
........................................................................ [ 35%]
|
||||||
|
........................................................................ [ 39%]
|
||||||
|
........................................................................ [ 43%]
|
||||||
|
........................................................................ [ 48%]
|
||||||
|
........................................................................ [ 52%]
|
||||||
|
........................................................................ [ 57%]
|
||||||
|
........................................................................ [ 61%]
|
||||||
|
........................................................................ [ 65%]
|
||||||
|
........................................................................ [ 70%]
|
||||||
|
........................................................................ [ 74%]
|
||||||
|
........................................................................ [ 79%]
|
||||||
|
........................................................................ [ 83%]
|
||||||
|
........................................................................ [ 87%]
|
||||||
|
........................................................................ [ 92%]
|
||||||
|
........................................................................ [ 96%]
|
||||||
|
..................................................... [100%]
|
||||||
|
=============================== warnings summary ===============================
|
||||||
|
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
|
||||||
|
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
|
||||||
|
from starlette.testclient import TestClient as TestClient # noqa
|
||||||
|
|
||||||
|
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
|
||||||
|
================================ tests coverage ================================
|
||||||
|
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
|
||||||
|
|
||||||
|
Name Stmts Miss Cover
|
||||||
|
-----------------------------------------------
|
||||||
|
app/__init__.py 1 0 100%
|
||||||
|
app/api/__init__.py 0 0 100%
|
||||||
|
app/api/auth.py 40 0 100%
|
||||||
|
app/api/chat.py 178 0 100%
|
||||||
|
app/api/chats.py 110 0 100%
|
||||||
|
app/api/config.py 7 0 100%
|
||||||
|
app/api/doc_drafts.py 93 0 100%
|
||||||
|
app/api/docs.py 50 0 100%
|
||||||
|
app/api/git_sources.py 212 0 100%
|
||||||
|
app/api/health.py 10 0 100%
|
||||||
|
app/api/steering.py 42 0 100%
|
||||||
|
app/api/suggestions.py 29 0 100%
|
||||||
|
app/api/sync.py 101 0 100%
|
||||||
|
app/api/tokens.py 28 0 100%
|
||||||
|
app/config.py 140 0 100%
|
||||||
|
app/core/__init__.py 0 0 100%
|
||||||
|
app/core/auth.py 45 0 100%
|
||||||
|
app/core/caching.py 108 0 100%
|
||||||
|
app/core/debugging.py 29 2 93%
|
||||||
|
app/core/docs_push.py 39 0 100%
|
||||||
|
app/core/logging.py 13 0 100%
|
||||||
|
app/core/tokens.py 33 0 100%
|
||||||
|
app/db.py 21 0 100%
|
||||||
|
app/main.py 62 0 100%
|
||||||
|
app/models.py 94 0 100%
|
||||||
|
app/rag/__init__.py 0 0 100%
|
||||||
|
app/rag/agent.py 222 0 100%
|
||||||
|
app/rag/archive_upload.py 128 0 100%
|
||||||
|
app/rag/chunker.py 206 4 98%
|
||||||
|
app/rag/git_sources.py 14 0 100%
|
||||||
|
app/rag/importer.py 180 3 98%
|
||||||
|
app/rag/llm.py 216 0 100%
|
||||||
|
app/rag/overview.py 71 0 100%
|
||||||
|
app/rag/prompts.py 88 0 100%
|
||||||
|
app/rag/retriever.py 150 3 98%
|
||||||
|
app/rag/scaffolding.py 55 0 100%
|
||||||
|
app/rag/source_removal.py 41 0 100%
|
||||||
|
app/rag/sources_meta.py 16 0 100%
|
||||||
|
app/rag/suggestions.py 27 0 100%
|
||||||
|
app/rag/summarizer.py 24 0 100%
|
||||||
|
app/schemas.py 231 0 100%
|
||||||
|
-----------------------------------------------
|
||||||
|
TOTAL 3154 12 99%
|
||||||
|
coverage gate: app/ 99% (>90%) OK
|
||||||
|
All checks passed!
|
||||||
|
0 errors, 0 warnings, 0 informations
|
||||||
|
validation OK
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
---
|
||||||
|
name: restore-test-db
|
||||||
|
description: Restores the large 1,000-document test knowledge base (the live-KB replica snapshot at data/bor_live_kb_replica.dump.sql) into the app's Postgres database in one atomic transaction — wipes the database's current contents, no re-embedding, no app code changes. Use when the user asks to restore the test database, the 1,000-document KB, or the big/live replica KB (e.g. "restore the test database", "put the 1000-doc KB back"). Do not confuse with the 8-document tool-calling fixture KB, which scripts/restore_test_kb.py restores.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Restore the 1,000-Document Test KB
|
||||||
|
|
||||||
|
Load `data/bor_live_kb_replica.dump.sql` (a full 99 MB `pg_dump` —
|
||||||
|
schema **and** data — of the live KB replica) into the app database
|
||||||
|
(`brain_of_reese`, per `BOR_DATABASE_URL` in `.env`). The embeddings come
|
||||||
|
with the dump, so there is **no re-embedding, no git clone, no app code
|
||||||
|
change** — the whole known state (documents, chunks + embeddings, source
|
||||||
|
registry, KB overview, alembic version) lands in a few seconds.
|
||||||
|
|
||||||
|
The dump is a local artifact: `data/` is gitignored and the dump is
|
||||||
|
**never committed**. It is not in git — if it is missing on a fresh
|
||||||
|
clone, stop and ask the user for it (it can only be made by `pg_dump`ing
|
||||||
|
the live/`bor_eval` KB).
|
||||||
|
|
||||||
|
## Rules (non-negotiable)
|
||||||
|
|
||||||
|
- **The restore is destructive** — it wipes the current contents of the
|
||||||
|
app database. Always take the disposable backup in step 2 first, even
|
||||||
|
when the database looks empty.
|
||||||
|
- **Do not edit app code** (`app/`, `tests/`, `alembic/`). This skill
|
||||||
|
swaps database contents, nothing else.
|
||||||
|
- **Run `psql` inside the db container** (`brain-of-reese_db_1`), not
|
||||||
|
with a host `psql` — the container's psql is 17.x, matching the dump
|
||||||
|
(newer dumps use the `\restrict` metacommand that old host psqls
|
||||||
|
choke on).
|
||||||
|
- This KB and the 8-document tool-calling fixture KB
|
||||||
|
(`scripts/restore_test_kb.py`) share the same database and overwrite
|
||||||
|
each other. Say which one is loaded in the summary.
|
||||||
|
|
||||||
|
## Procedure
|
||||||
|
|
||||||
|
All commands run from the repo root.
|
||||||
|
|
||||||
|
### 1. Preconditions (fast fail)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
podman compose up -d db
|
||||||
|
ls -lh data/bor_live_kb_replica.dump.sql # must exist, ~99 MB
|
||||||
|
```
|
||||||
|
|
||||||
|
Check the dump's schema version against the current head:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep -A1 '^COPY public.alembic_version' data/bor_live_kb_replica.dump.sql | tail -1
|
||||||
|
uv run alembic heads
|
||||||
|
```
|
||||||
|
|
||||||
|
The two must match (the 2026-09-05 snapshot carries `0011`). If the dump
|
||||||
|
is **older** than head, proceed and run `uv run alembic upgrade head`
|
||||||
|
after step 3. If the dump is **newer** than head, the local code is
|
||||||
|
behind — stop and flag it to the user.
|
||||||
|
|
||||||
|
### 2. Disposable safety backup
|
||||||
|
|
||||||
|
Seconds, in `/tmp` (never committed):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
podman exec brain-of-reese_db_1 pg_dump -U reese brain_of_reese -Fc \
|
||||||
|
> /tmp/pre_restore_$(date +%Y%m%d_%H%M%S).dump
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Reset the schema, load the dump (one transaction)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
podman exec brain-of-reese_db_1 psql -U reese -d brain_of_reese -c \
|
||||||
|
"DROP SCHEMA public CASCADE; CREATE SCHEMA public; GRANT ALL ON SCHEMA public TO PUBLIC;"
|
||||||
|
|
||||||
|
podman exec -i brain-of-reese_db_1 psql -U reese -d brain_of_reese \
|
||||||
|
-v ON_ERROR_STOP=1 --single-transaction -q -f /dev/stdin \
|
||||||
|
< data/bor_live_kb_replica.dump.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
Why the schema reset: the dump contains plain `CREATE TABLE` (no
|
||||||
|
`IF NOT EXISTS`, no `--clean`), so it can only load into an empty
|
||||||
|
`public` schema. Why `--single-transaction` + `ON_ERROR_STOP`: a failed
|
||||||
|
load rolls back instead of leaving a half-loaded KB. Known failure mode:
|
||||||
|
the `DROP SCHEMA` is its own committed statement, so a failed load leaves
|
||||||
|
an **empty** schema — that is fine, fix the cause and re-run step 3
|
||||||
|
(the reset is idempotent).
|
||||||
|
|
||||||
|
### 4. Verify the fingerprint
|
||||||
|
|
||||||
|
```bash
|
||||||
|
podman exec brain-of-reese_db_1 psql -U reese -d brain_of_reese -tAc \
|
||||||
|
"select 'docs='||count(*) from documents
|
||||||
|
union all select 'chunks='||count(*) from chunks
|
||||||
|
union all select 'sources='||string_agg(distinct source, ',' order by source) from documents
|
||||||
|
union all select 'alembic='||(select version_num from alembic_version);"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected for the 2026-09-05 snapshot: **docs=1000, chunks=8866,
|
||||||
|
sources=deploy,homelab,homepage,ServMon,skills,vibe-bot, alembic=0011**.
|
||||||
|
If any number differs, the dump file is a different snapshot — report the
|
||||||
|
actual numbers to the user; do not guess.
|
||||||
|
|
||||||
|
### 5. Dev-server check (only if it is running)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pgrep -af "uvicorn app.main" && curl -s http://localhost:8000/api/health
|
||||||
|
```
|
||||||
|
|
||||||
|
Expect `"db":"up"` — the SQLAlchemy pool reconnects fine across the
|
||||||
|
schema reset, so no restart is normally needed. If the server misbehaves
|
||||||
|
after the restore, restart it (`uv run uvicorn app.main:app --reload`).
|
||||||
|
|
||||||
|
### 6. Summary
|
||||||
|
|
||||||
|
One line per verified number (docs / chunks / sources / alembic), the
|
||||||
|
wall time, and a reminder of which KB is loaded: this is the
|
||||||
|
**1,000-document live replica**; the 8-document tool-calling fixture is
|
||||||
|
brought back separately with `uv run python -m scripts.restore_test_kb`.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
- **`database unreachable` / connection refused** — the db container is
|
||||||
|
down; `podman compose up -d db`, then re-run from step 1.
|
||||||
|
- **`relation "public.<table>" already exists`** — the schema was not
|
||||||
|
reset (or a previous load failed after the reset rolled back); re-run
|
||||||
|
the `DROP SCHEMA` command from step 3.
|
||||||
|
- **`psql: /dev/stdin:N: unknown command '\restrict'`** — you ran a host
|
||||||
|
`psql`; use the container's, per the rules.
|
||||||
|
- **load succeeds but counts are wrong** — compare the dump's
|
||||||
|
`alembic_version` (step 1) and `documents` row count
|
||||||
|
(`sed -n '/^COPY public.documents/,/^\\./p' data/bor_live_kb_replica.dump.sql | wc -l`,
|
||||||
|
minus 2) against the loaded numbers; a mismatch means the dump was
|
||||||
|
truncated in transfer — ask the user for the original file.
|
||||||
|
- **`schema not applied`-style errors from the app afterwards** — run
|
||||||
|
`uv run alembic current`; if the dump was older than head, step 1's
|
||||||
|
`alembic upgrade head` was the missing piece.
|
||||||
+1
-1
@@ -52,7 +52,7 @@ BOR_RRF_K=60 # Reciprocal Rank Fusion damping constant
|
|||||||
# your own (e.g. md,sh,toml) or narrow it (e.g. md). A blank list or a
|
# your own (e.g. md,sh,toml) or narrow it (e.g. md). A blank list or a
|
||||||
# malformed token (e.g. md,sh!) fails startup loudly, naming the value.
|
# malformed token (e.g. md,sh!) fails startup loudly, naming the value.
|
||||||
BOR_IMPORT_EXTENSIONS=md,markdown,txt,yaml,yml,json,py,container,network,volume,image,pod,kube,swap,os,endpoint,j2
|
BOR_IMPORT_EXTENSIONS=md,markdown,txt,yaml,yml,json,py,container,network,volume,image,pod,kube,swap,os,endpoint,j2
|
||||||
# BOR_SUGGESTIONS=["How is my Kubernetes cluster set up?"] # JSON list of onboarding chips
|
# BOR_SUGGESTIONS=["How is my Kubernetes cluster set up?"] # JSON seed chips — shown only before any question has been saved (phase 80)
|
||||||
|
|
||||||
# --- Import sources (git; phase 28, admin-managed since phase 35) ---
|
# --- Import sources (git; phase 28, admin-managed since phase 35) ---
|
||||||
# Comma-separated git repo URLs; import_docs clones each (first run) or
|
# Comma-separated git repo URLs; import_docs clones each (first run) or
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ COPY frontend ./
|
|||||||
RUN mkdir -p /out/assets \
|
RUN mkdir -p /out/assets \
|
||||||
&& esbuild ./assets/app.js --bundle --minify --format=esm --target=es2022 --outfile=/out/assets/app.js \
|
&& esbuild ./assets/app.js --bundle --minify --format=esm --target=es2022 --outfile=/out/assets/app.js \
|
||||||
&& esbuild ./assets/router.js --bundle --minify --format=esm --target=es2022 --outfile=/out/assets/router.js \
|
&& esbuild ./assets/router.js --bundle --minify --format=esm --target=es2022 --outfile=/out/assets/router.js \
|
||||||
|
&& esbuild ./assets/token-gate.js --bundle --minify --format=esm --target=es2022 --outfile=/out/assets/token-gate.js \
|
||||||
&& esbuild ./assets/document.js --bundle --minify --format=esm --target=es2022 --outfile=/out/assets/document.js \
|
&& esbuild ./assets/document.js --bundle --minify --format=esm --target=es2022 --outfile=/out/assets/document.js \
|
||||||
&& esbuild ./assets/login.js --bundle --minify --format=esm --target=es2022 --outfile=/out/assets/login.js \
|
&& esbuild ./assets/login.js --bundle --minify --format=esm --target=es2022 --outfile=/out/assets/login.js \
|
||||||
&& esbuild ./assets/shared.js --bundle --minify --format=esm --target=es2022 --outfile=/out/assets/shared.js \
|
&& esbuild ./assets/shared.js --bundle --minify --format=esm --target=es2022 --outfile=/out/assets/shared.js \
|
||||||
|
|||||||
@@ -108,7 +108,10 @@ uv run uvicorn app.main:app --reload
|
|||||||
|
|
||||||
- **Chat** (`/`) — ask questions; answers stream in with **source chips**
|
- **Chat** (`/`) — ask questions; answers stream in with **source chips**
|
||||||
that cite the exact documents used. Clicking a chip opens that document
|
that cite the exact documents used. Clicking a chip opens that document
|
||||||
in an **almost-fullscreen modal** on the same page (no new tab).
|
in an **almost-fullscreen modal** on the same page (no new tab). The
|
||||||
|
onboarding suggestion chips follow the last 3 questions asked — on a
|
||||||
|
fresh deployment (no saved questions yet) they seed from
|
||||||
|
`BOR_SUGGESTIONS`.
|
||||||
- **Document viewer** — the modal above *is* the viewer; the full text of
|
- **Document viewer** — the modal above *is* the viewer; the full text of
|
||||||
any indexed document is served from the database (no filesystem access):
|
any indexed document is served from the database (no filesystem access):
|
||||||
markdown is rendered, every other format (`yaml`, `json`, `py`, `txt`, …)
|
markdown is rendered, every other format (`yaml`, `json`, `py`, `txt`, …)
|
||||||
@@ -119,7 +122,8 @@ uv run uvicorn app.main:app --reload
|
|||||||
- **Sources** (`/sources.html`) — the indexed document list; the *Path*
|
- **Sources** (`/sources.html`) — the indexed document list; the *Path*
|
||||||
column opens each document in the same **almost-fullscreen modal** (no
|
column opens each document in the same **almost-fullscreen modal** (no
|
||||||
new tab). **Admin-only** — anonymous visitors see a sign-in gate instead
|
new tab). **Admin-only** — anonymous visitors see a sign-in gate instead
|
||||||
(the catalog is what the login locks; the document viewer itself stays
|
(the catalog is what the admin login locks; the document viewer asks
|
||||||
|
for an access token — see [API tokens](#api-tokens). Shared chats stay
|
||||||
open to everyone).
|
open to everyone).
|
||||||
- **Git sources** (`/git-sources.html`) — the admin-managed source
|
- **Git sources** (`/git-sources.html`) — the admin-managed source
|
||||||
registry: the git repositories the **Sync sources** button clones and
|
registry: the git repositories the **Sync sources** button clones and
|
||||||
@@ -214,10 +218,12 @@ unchanged.
|
|||||||
## Admin & sign-in
|
## Admin & sign-in
|
||||||
|
|
||||||
Brain of Reese has exactly **one account: the admin (you)**. Signing in
|
Brain of Reese has exactly **one account: the admin (you)**. Signing in
|
||||||
unlocks the **full Sources catalog** and the **answer-tuning** controls;
|
unlocks the **full Sources catalog**, the **answer-tuning** controls, and
|
||||||
everyone else stays anonymous and keeps **chat** and the **document
|
**token management**. **Shared chats** are the only content that stays
|
||||||
viewer** (any document an answer cites can be opened by its direct URL —
|
open to anonymous visitors; everyone in between gets an **API token** —
|
||||||
the catalog is gated, not the viewer).
|
the admin generates one in the **Tokens** view and hands it out, and the
|
||||||
|
holder enters it at the in-app gate to use chat and open the documents
|
||||||
|
answers cite (see [API tokens](#api-tokens) below).
|
||||||
|
|
||||||
### Setup (one-time)
|
### Setup (one-time)
|
||||||
|
|
||||||
@@ -247,8 +253,14 @@ Set the missing variable(s): BOR_ADMIN_PASSWORD, BOR_SESSION_SECRET …
|
|||||||
message — no user enumeration, there is only one user).
|
message — no user enumeration, there is only one user).
|
||||||
- `POST /api/logout` → `204` (session cleared and cookie expired;
|
- `POST /api/logout` → `204` (session cleared and cookie expired;
|
||||||
idempotent for anonymous callers).
|
idempotent for anonymous callers).
|
||||||
- `GET /api/whoami` → `{"authenticated": bool, "role": "admin"|"anonymous"}`
|
- `GET /api/whoami` → `{"authenticated": bool, "role": "admin"|"user"|"anonymous"}`
|
||||||
— the single source of truth for every UI gating decision.
|
— the single source of truth for every UI gating decision
|
||||||
|
(`authenticated` is true for **both** admin and token users; the UI
|
||||||
|
gates on `role === "admin"`).
|
||||||
|
- `POST /api/token-auth {"token": "bor_…"}` → `204` + the **same signed
|
||||||
|
cookie** (role `user`); malformed / unknown / revoked all get one
|
||||||
|
generic `401` `{"detail": "invalid token"}` — no enumeration. The two
|
||||||
|
roles coexist in one session; **Sign out** clears both at once.
|
||||||
- Cookie flags: `same_site="lax"`, `https_only` off — **no HTTPS
|
- Cookie flags: `same_site="lax"`, `https_only` off — **no HTTPS
|
||||||
enforcement on purpose** (homelab HTTP; the cookie is single-admin
|
enforcement on purpose** (homelab HTTP; the cookie is single-admin
|
||||||
convenience, not a cloud boundary). Max age `BOR_SESSION_MAX_AGE`
|
convenience, not a cloud boundary). Max age `BOR_SESSION_MAX_AGE`
|
||||||
@@ -258,12 +270,41 @@ Set the missing variable(s): BOR_ADMIN_PASSWORD, BOR_SESSION_SECRET …
|
|||||||
|
|
||||||
### Who can do what
|
### Who can do what
|
||||||
|
|
||||||
| Capability | Anonymous | Admin (signed in) |
|
| Capability | Anonymous | Token user | Admin (signed in) |
|
||||||
|---|---|---|
|
|---|---|---|---|
|
||||||
| Chat (`/`) + suggestion chips | yes | yes |
|
| Shared chats (`/shared/<token>`) | yes | yes | yes |
|
||||||
| Document viewer (`/document.html?source=…&path=…`) | yes — any indexed doc by direct URL | yes |
|
| Chat (`/`) + suggestion chips | token gate | yes | yes |
|
||||||
| Sources catalog (`/sources.html`, `GET /api/docs`) | sign-in gate | full catalog |
|
| Document viewer (`/document.html?source=…&path=…`) | token gate | yes | yes |
|
||||||
| Tuning (Tune button, Tuning panel, `/api/steering`) | UI hidden | full |
|
| Sources catalog (`/sources.html`, `GET /api/docs`) | sign-in gate | 403 | full catalog |
|
||||||
|
| Tuning (Tune button, Tuning panel, `/api/steering`) | UI hidden | 403 | full |
|
||||||
|
| Saved-chat history (`/history.html`) | sign-in gate | no History view | full |
|
||||||
|
|
||||||
|
### API tokens
|
||||||
|
|
||||||
|
The admin can hand out access without sharing the admin password.
|
||||||
|
|
||||||
|
- **Generate:** sign in as admin → **Tokens** in the navbar
|
||||||
|
(`/tokens.html`). Type a label (e.g. `alice`) and hit **Generate** — a
|
||||||
|
`bor_` + 32-hex token appears in the *shown once* block. Copy it now:
|
||||||
|
only its SHA-256 hash is stored, so the plaintext can never be
|
||||||
|
retrieved again.
|
||||||
|
- **Use:** the holder opens the app (or a direct document URL) and gets
|
||||||
|
the **token gate** instead of content — they enter the token, and the
|
||||||
|
browser caches it in `localStorage["bor.token"]`, silently re-sending
|
||||||
|
it on every page load (no re-entry on reloads or new tabs). **Sign
|
||||||
|
out** clears the cache; a failed silent re-auth (e.g. a revoked token)
|
||||||
|
drops the cached copy and shows the gate again. Private-mode browsers
|
||||||
|
still work — the gate just can't cache.
|
||||||
|
- **Scope:** a token user can chat (with suggestion chips) and open the
|
||||||
|
documents answers cite — nothing else. The Sources catalog, git
|
||||||
|
sources, tuning, document drafts, and the saved-chat history stay
|
||||||
|
admin-only (`403` on their APIs; no History view). **Shared chats
|
||||||
|
stay open to everyone** — the only anonymous content.
|
||||||
|
- **Revoke:** **Revoke** on the token's row (inline two-step confirm).
|
||||||
|
Revocation is **immediate**: the token's next request — including a
|
||||||
|
fresh login attempt — is refused with the same generic `401` as a bad
|
||||||
|
token. Revoked tokens stay in the list, marked **Revoked**, with their
|
||||||
|
last used time.
|
||||||
|
|
||||||
The public API endpoints stay stateless — the signed cookie is the only
|
The public API endpoints stay stateless — the signed cookie is the only
|
||||||
session state in the system.
|
session state in the system.
|
||||||
@@ -779,7 +820,7 @@ served locally (no CDN), `BOR_ENVIRONMENT=production`.
|
|||||||
| `BOR_SUMMARY_MAX_CHARS` | `12000` | cap on document content sent to the `lite` summary model at import (see *Document summaries*) |
|
| `BOR_SUMMARY_MAX_CHARS` | `12000` | cap on document content sent to the `lite` summary model at import (see *Document summaries*) |
|
||||||
| `BOR_KB_OVERVIEW_MAX_CHARS` | `4000` | char budget for the `<knowledge_base>` (KB overview) prompt section |
|
| `BOR_KB_OVERVIEW_MAX_CHARS` | `4000` | char budget for the `<knowledge_base>` (KB overview) prompt section |
|
||||||
| `BOR_OVERVIEW_INPUT_MAX_CHARS` | `40000` | cap on the document list sent to the `lite` model when generating the KB overview |
|
| `BOR_OVERVIEW_INPUT_MAX_CHARS` | `40000` | cap on the document list sent to the `lite` model when generating the KB overview |
|
||||||
| `BOR_SUGGESTIONS` | built-in list | JSON list of onboarding chips |
|
| `BOR_SUGGESTIONS` | built-in list | JSON seed for the onboarding chips — shown only before the first saved question; afterwards the chips are the last 3 questions asked (phase 80) |
|
||||||
| `BOR_ADMIN_PASSWORD` | *(required)* | the single admin's password (plaintext, `.env`); app refuses to start when empty |
|
| `BOR_ADMIN_PASSWORD` | *(required)* | the single admin's password (plaintext, `.env`); app refuses to start when empty |
|
||||||
| `BOR_SESSION_SECRET` | *(required)* | signing key for the `bor_session` cookie; `python -c 'import secrets;print(secrets.token_hex(32))'` |
|
| `BOR_SESSION_SECRET` | *(required)* | signing key for the `bor_session` cookie; `python -c 'import secrets;print(secrets.token_hex(32))'` |
|
||||||
| `BOR_SESSION_MAX_AGE` | `43200` | session-cookie lifetime in seconds (12 h, sliding) |
|
| `BOR_SESSION_MAX_AGE` | `43200` | session-cookie lifetime in seconds (12 h, sliding) |
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
"""api_tokens: admin-issued access tokens (phase 79)
|
||||||
|
|
||||||
|
Revision ID: 0012
|
||||||
|
Revises: 0011
|
||||||
|
Create Date: 2026-09-07
|
||||||
|
|
||||||
|
Phase 79 (API tokens: the admin generates named tokens and hands them
|
||||||
|
out so people can sign in to the app — the ONLY anonymous content left
|
||||||
|
is the shared chats; extends the phase-16 single-admin auth, one
|
||||||
|
additive, reversible table, no other schema change, A13):
|
||||||
|
|
||||||
|
* ``api_tokens`` — one row per admin-generated token (``bor_`` + 32 hex
|
||||||
|
chars — ``secrets.token_hex(16)``, service in task 02). The
|
||||||
|
**plaintext exists only in the 201 create response, returned exactly
|
||||||
|
once**; the row never carries it.
|
||||||
|
* ``token_hash`` — the stored credential: the SHA-256 hex digest of the
|
||||||
|
**full** token string (String(64) — the ``documents.content_hash``
|
||||||
|
precedent). Hashing the full string, not the suffix, so a stripped
|
||||||
|
prefix can never collide. Lineage of ``saved_chats.share_token`` /
|
||||||
|
``doc_drafts.token`` — but HASHED: unlike those unguessable
|
||||||
|
``uuid4`` link tokens these are long-lived hand-out credentials, and
|
||||||
|
a leaked database must not hand anyone working tokens.
|
||||||
|
* ``ix_api_tokens_token_hash`` — UNIQUE index on ``token_hash``: the
|
||||||
|
lookup key (a unique-index hit — sha256's pre-image resistance means
|
||||||
|
there is no enumeration surface beyond the lookup). The explicit
|
||||||
|
``op.create_index`` shape of ``ix_saved_chats_share_token`` (0009)
|
||||||
|
and ``ix_doc_drafts_token`` (0011).
|
||||||
|
* ``label`` — String(120) NOT NULL: the hand-out name (e.g. "alice") —
|
||||||
|
display-only: no index, not unique (two tokens may share a label).
|
||||||
|
* ``created_at`` — TIMESTAMPTZ NOT NULL, server default now().
|
||||||
|
* ``last_used_at`` / ``revoked_at`` — TIMESTAMPTZ NULL: bumped/set by
|
||||||
|
the service (tasks 02/03); NULL until the token is first used /
|
||||||
|
revoked.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
revision = "0012"
|
||||||
|
down_revision = "0011"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.create_table(
|
||||||
|
"api_tokens",
|
||||||
|
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
|
||||||
|
sa.Column("label", sa.String(length=120), nullable=False),
|
||||||
|
sa.Column("token_hash", sa.String(length=64), nullable=False),
|
||||||
|
sa.Column(
|
||||||
|
"created_at",
|
||||||
|
sa.DateTime(timezone=True),
|
||||||
|
server_default=sa.func.now(),
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
|
sa.Column("last_used_at", sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.Column("revoked_at", sa.DateTime(timezone=True), nullable=True),
|
||||||
|
)
|
||||||
|
# The hash is the stored credential — the unique lookup handle (the
|
||||||
|
# saved_chats.share_token / doc_drafts.token unique-index precedent,
|
||||||
|
# phases 51/59).
|
||||||
|
op.create_index("ix_api_tokens_token_hash", "api_tokens", ["token_hash"], unique=True)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# Safe order: drop the token-hash index first, then the table (A13 —
|
||||||
|
# fully reversible, no other schema change).
|
||||||
|
op.drop_index("ix_api_tokens_token_hash", table_name="api_tokens")
|
||||||
|
op.drop_table("api_tokens")
|
||||||
+86
-19
@@ -1,24 +1,42 @@
|
|||||||
"""Auth API — single-admin sign-in (phase 16; A10 revised 2026-08-22).
|
"""Auth API — admin sign-in (phase 16) + token login (phase 79).
|
||||||
|
|
||||||
* ``POST /api/login`` — 204 + signed session cookie on success; 401
|
* ``POST /api/login`` — 204 + signed session cookie on success; 401
|
||||||
``invalid password`` on any mismatch (constant-time, one generic
|
``invalid password`` on any mismatch (constant-time, one generic
|
||||||
message, no session set).
|
message, no session set).
|
||||||
* ``POST /api/logout`` — 204; clears the session and expires the cookie
|
* ``POST /api/token-auth`` — 204 + signed session cookie for a valid,
|
||||||
(idempotent for anonymous callers).
|
unrevoked API token (PUBLIC — it is the token holders' login route);
|
||||||
|
every failure shape (malformed / unknown / revoked / empty) is ONE
|
||||||
|
generic 401 ``invalid token`` (no enumeration, the phase-16 pattern).
|
||||||
|
* ``POST /api/logout`` — 204; clears the session and expires the
|
||||||
|
cookie (idempotent for anonymous callers — one logout wipes BOTH
|
||||||
|
roles, the session is one dict).
|
||||||
* ``GET /api/whoami`` — ``{"authenticated": bool, "role":
|
* ``GET /api/whoami`` — ``{"authenticated": bool, "role":
|
||||||
"admin"|"anonymous"}``; the single source of truth for all UI gating.
|
"admin"|"user"|"anonymous"}``; the single source of truth for all UI
|
||||||
|
gating (phase 79: the third role; the UI's admin-only surfaces key
|
||||||
|
off ``role === "admin"`` specifically).
|
||||||
|
|
||||||
The public API otherwise stays stateless (A10): chat, the document
|
The rest of the public surface is the phase-79 contract: health,
|
||||||
content endpoint (soft rule — anonymous may open any document by direct
|
config, whoami, login, token-auth, and the shared chats stay
|
||||||
URL), suggestions, and health never require the cookie.
|
anonymous — chat, suggestions, and the document viewer content are
|
||||||
|
user-gated (``require_user``).
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException, Request, Response
|
from fastapi import APIRouter, Depends, HTTPException, Request, Response
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.config import get_settings
|
from app.config import get_settings
|
||||||
from app.core.auth import ADMIN_SESSION_KEY, check_password, sign_in, sign_out
|
from app.core import tokens as token_service
|
||||||
from app.schemas import LoginRequest, WhoamiResponse
|
from app.core.auth import (
|
||||||
|
ADMIN_SESSION_KEY,
|
||||||
|
USER_SESSION_KEY,
|
||||||
|
USER_TOKEN_ID_KEY,
|
||||||
|
check_password,
|
||||||
|
sign_in,
|
||||||
|
sign_out,
|
||||||
|
)
|
||||||
|
from app.db import get_db
|
||||||
|
from app.schemas import LoginRequest, TokenAuthRequest, WhoamiResponse
|
||||||
|
|
||||||
router = APIRouter(tags=["auth"])
|
router = APIRouter(tags=["auth"])
|
||||||
|
|
||||||
@@ -39,14 +57,48 @@ def login(payload: LoginRequest, request: Request) -> Response:
|
|||||||
raise HTTPException(status_code=401, detail="invalid password")
|
raise HTTPException(status_code=401, detail="invalid password")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/token-auth", status_code=204)
|
||||||
|
def token_auth(
|
||||||
|
payload: TokenAuthRequest,
|
||||||
|
request: Request,
|
||||||
|
db: Session = Depends(get_db), # noqa: B008
|
||||||
|
) -> Response:
|
||||||
|
"""Sign a token holder in (phase 79 — PUBLIC: this IS the login).
|
||||||
|
|
||||||
|
A valid, unrevoked token → 204 + the signed ``bor_session`` cookie
|
||||||
|
(the same SessionMiddleware mechanism as ``/api/login``): the
|
||||||
|
session carries the ``user`` key plus the token's row id
|
||||||
|
(``user_token_id``), and ``require_user``'s live row check enforces
|
||||||
|
revocation from the holder's very next request. The token's
|
||||||
|
``last_used_at`` is stamped here (the admin's Tokens view shows the
|
||||||
|
login as the last use).
|
||||||
|
|
||||||
|
EVERY failure shape — malformed, unknown, revoked, empty — is ONE
|
||||||
|
generic 401 ``invalid token``: the lookup is by hash (the service
|
||||||
|
returns ``None`` for anything that is not an active row), so there
|
||||||
|
is no enumeration surface (the phase-16 pattern).
|
||||||
|
"""
|
||||||
|
token = payload.token.strip()
|
||||||
|
row = token_service.find_active_by_token(db, token) if token else None
|
||||||
|
if row is None:
|
||||||
|
raise HTTPException(status_code=401, detail="invalid token")
|
||||||
|
token_service.mark_used(row)
|
||||||
|
db.commit()
|
||||||
|
request.session[USER_SESSION_KEY] = True
|
||||||
|
request.session[USER_TOKEN_ID_KEY] = str(row.id)
|
||||||
|
return Response(status_code=204)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/logout", status_code=204)
|
@router.post("/logout", status_code=204)
|
||||||
def logout(request: Request, response: Response) -> Response:
|
def logout(request: Request, response: Response) -> Response:
|
||||||
"""Sign out: clear the session AND expire the browser cookie.
|
"""Sign out: clear the session AND expire the browser cookie.
|
||||||
|
|
||||||
``sign_out`` empties the session dict (which the middleware does not
|
``sign_out`` empties the session dict (which the middleware does not
|
||||||
re-persist — an empty session has nothing to sign), so this route also
|
re-persist — an empty session has nothing to sign), so this route
|
||||||
sends ``delete_cookie`` to make the browser drop the signed cookie
|
also sends ``delete_cookie`` to make the browser drop the signed
|
||||||
right now. Idempotent: an anonymous logout is still a 204.
|
cookie right now. One logout wipes BOTH roles (admin and token —
|
||||||
|
the session is one dict). Idempotent: an anonymous logout is still
|
||||||
|
a 204.
|
||||||
"""
|
"""
|
||||||
sign_out(request.session)
|
sign_out(request.session)
|
||||||
response.delete_cookie(get_settings().session_cookie, path="/")
|
response.delete_cookie(get_settings().session_cookie, path="/")
|
||||||
@@ -55,9 +107,24 @@ def logout(request: Request, response: Response) -> Response:
|
|||||||
|
|
||||||
@router.get("/whoami", response_model=WhoamiResponse)
|
@router.get("/whoami", response_model=WhoamiResponse)
|
||||||
def whoami(request: Request) -> WhoamiResponse:
|
def whoami(request: Request) -> WhoamiResponse:
|
||||||
"""Who is the caller? Drives every UI gating decision (phase 16)."""
|
"""Who is the caller? Drives every UI gating decision.
|
||||||
authenticated = bool(request.session.get(ADMIN_SESSION_KEY))
|
|
||||||
return WhoamiResponse(
|
Three roles (phase 79): ``admin`` (the signed-in admin — wins when
|
||||||
authenticated=authenticated,
|
the browser holds BOTH an admin and a token session), ``user`` (a
|
||||||
role="admin" if authenticated else "anonymous",
|
token holder), ``anonymous``. ``authenticated`` is true for admin
|
||||||
)
|
AND user; the UI's admin-only surfaces key off ``role === "admin"``
|
||||||
|
specifically, not off ``authenticated``.
|
||||||
|
|
||||||
|
This endpoint only reads the session keys — it does NOT live-check
|
||||||
|
the token row. A token session whose row was just revoked still
|
||||||
|
reports ``user`` here until the next gated request: ``require_user``
|
||||||
|
then pops the dead keys, after which this endpoint reports
|
||||||
|
anonymous (the phase-79 live-enforcement contract).
|
||||||
|
"""
|
||||||
|
if request.session.get(ADMIN_SESSION_KEY):
|
||||||
|
role = "admin"
|
||||||
|
elif request.session.get(USER_SESSION_KEY):
|
||||||
|
role = "user"
|
||||||
|
else:
|
||||||
|
role = "anonymous"
|
||||||
|
return WhoamiResponse(authenticated=role != "anonymous", role=role)
|
||||||
|
|||||||
+8
-1
@@ -149,6 +149,7 @@ from sqlalchemy.orm import Session
|
|||||||
|
|
||||||
from app.api.steering import load_steering_notes
|
from app.api.steering import load_steering_notes
|
||||||
from app.config import Settings, get_settings
|
from app.config import Settings, get_settings
|
||||||
|
from app.core.auth import require_user
|
||||||
from app.db import db_available, get_db
|
from app.db import db_available, get_db
|
||||||
from app.models import Document, QueryLog
|
from app.models import Document, QueryLog
|
||||||
from app.rag.agent import (
|
from app.rag.agent import (
|
||||||
@@ -296,10 +297,16 @@ def plan_turn(
|
|||||||
@router.post("/chat")
|
@router.post("/chat")
|
||||||
async def chat(
|
async def chat(
|
||||||
request: ChatRequest,
|
request: ChatRequest,
|
||||||
|
_user: None = Depends(require_user), # noqa: B008 # phase 79: admin or live token
|
||||||
db: Session = Depends(get_db), # noqa: B008
|
db: Session = Depends(get_db), # noqa: B008
|
||||||
llm: LLMClient = Depends(get_llm), # noqa: B008
|
llm: LLMClient = Depends(get_llm), # noqa: B008
|
||||||
):
|
):
|
||||||
"""One chat turn: SSE stream of ``delta`` events + a final ``done``."""
|
"""One chat turn: SSE stream of ``delta`` events + a final ``done``.
|
||||||
|
|
||||||
|
User-gated (phase 79): anonymous callers get 401 ``authentication
|
||||||
|
required`` before any streaming — the ONLY anonymous content is the
|
||||||
|
shared chats.
|
||||||
|
"""
|
||||||
if not db_available():
|
if not db_available():
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
status_code=503,
|
status_code=503,
|
||||||
|
|||||||
+17
-11
@@ -19,7 +19,7 @@ from sqlalchemy import func, select
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.api.sync import _sanitize_error
|
from app.api.sync import _sanitize_error
|
||||||
from app.core.auth import require_admin
|
from app.core.auth import require_admin, require_user
|
||||||
from app.db import get_db
|
from app.db import get_db
|
||||||
from app.models import Chunk, Document
|
from app.models import Chunk, Document
|
||||||
from app.rag.llm import EmbeddingError, LLMClient
|
from app.rag.llm import EmbeddingError, LLMClient
|
||||||
@@ -42,11 +42,12 @@ def list_indexed_documents(
|
|||||||
) -> DocList:
|
) -> DocList:
|
||||||
"""All indexed documents with per-document chunk counts.
|
"""All indexed documents with per-document chunk counts.
|
||||||
|
|
||||||
Admin-only (phase 16 — the catalog is what the sign-in gates; the
|
Admin-only (phase 16 — the catalog is what the admin sign-in gates;
|
||||||
document viewer itself stays public, see below). Anonymous callers
|
the document viewer below is user-gated since phase 79, the shared
|
||||||
get 403 ``admin only`` and the Sources page renders its sign-in gate
|
chats being the only anonymous surface). Anonymous callers get 403
|
||||||
instead. An empty list means the knowledge base has not been imported
|
``admin only`` and the Sources page renders its sign-in gate instead.
|
||||||
yet — the Sources page renders its designed empty state in that case.
|
An empty list means the knowledge base has not been imported yet —
|
||||||
|
the Sources page renders its designed empty state in that case.
|
||||||
"""
|
"""
|
||||||
rows = db.execute(
|
rows = db.execute(
|
||||||
select(
|
select(
|
||||||
@@ -78,7 +79,10 @@ def list_indexed_documents(
|
|||||||
|
|
||||||
@router.get("/documents/content", response_model=DocContent)
|
@router.get("/documents/content", response_model=DocContent)
|
||||||
def get_document_content(
|
def get_document_content(
|
||||||
source: str, path: str, db: Session = Depends(get_db) # noqa: B008
|
source: str,
|
||||||
|
path: str,
|
||||||
|
db: Session = Depends(get_db), # noqa: B008
|
||||||
|
_user: None = Depends(require_user), # noqa: B008 # phase 79: admin or live token
|
||||||
) -> DocContent:
|
) -> DocContent:
|
||||||
"""Full content of one indexed document, looked up by ``(source, path)``.
|
"""Full content of one indexed document, looked up by ``(source, path)``.
|
||||||
|
|
||||||
@@ -86,10 +90,12 @@ def get_document_content(
|
|||||||
strings such as ``../../etc/passwd`` — are just non-existent rows and
|
strings such as ``../../etc/passwd`` — are just non-existent rows and
|
||||||
map to 404 ``{detail: "document not found"}``.
|
map to 404 ``{detail: "document not found"}``.
|
||||||
|
|
||||||
Deliberately PUBLIC for anonymous callers (phase 16 soft rule, owner
|
User-gated (phase 79 — SUPERSEDES the phase-16 "deliberately PUBLIC
|
||||||
decision 2026-08-22): the *catalog* (``GET /api/docs``) is what the
|
(soft rule)" note, owner decision 2026-08-22): the viewer content is
|
||||||
sign-in gates, not the viewer — chat cites documents and anyone may
|
token-or-admin like the rest of the app surface — chat cites
|
||||||
open a cited document by direct URL.
|
documents and a signed-in user (admin or token holder) opens a cited
|
||||||
|
document by direct URL. The ONLY anonymous content left is the
|
||||||
|
shared chats.
|
||||||
"""
|
"""
|
||||||
row = db.execute(
|
row = db.execute(
|
||||||
select(Document, func.count(Chunk.id).label("chunks"))
|
select(Document, func.count(Chunk.id).label("chunks"))
|
||||||
|
|||||||
+3
-2
@@ -10,8 +10,9 @@ into the system prompt of **every** chat turn as the ``<tuning>`` section
|
|||||||
replacement of ``note``, ``created_at`` preserved), ``DELETE /{note_id}``.
|
replacement of ``note``, ``created_at`` preserved), ``DELETE /{note_id}``.
|
||||||
The whole router sits behind :func:`app.core.auth.require_admin` —
|
The whole router sits behind :func:`app.core.auth.require_admin` —
|
||||||
anonymous callers get 403 on every steering route (the chat turn itself
|
anonymous callers get 403 on every steering route (the chat turn itself
|
||||||
reads the table in-process and stays public); the PUT route adds no auth
|
reads the table in-process; since phase 79 that turn is user-gated
|
||||||
surface of its own, it reuses that router-level dependency.
|
through ``require_user``); the PUT route adds no auth surface of its
|
||||||
|
own, it reuses that router-level dependency.
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|||||||
+78
-4
@@ -1,14 +1,88 @@
|
|||||||
"""Suggested-question endpoint (drives the onboarding chips in the UI)."""
|
"""Suggested-question endpoint (drives the onboarding chips in the UI).
|
||||||
|
|
||||||
|
Phase 80: the chips are the **last 3 questions asked** — the three
|
||||||
|
most recent user questions across ALL saved chats (chats walked
|
||||||
|
newest-``updated_at`` first, each chat's messages walked newest-first,
|
||||||
|
exact de-duplicated, cap 3 — see :func:`last_questions`). A fresh
|
||||||
|
deployment — zero saved questions — gets the seed list instead
|
||||||
|
(``BOR_SUGGESTIONS`` override, or the built-in default).
|
||||||
|
|
||||||
|
Phase 79: user-gated (``require_user``) — the chips are part of the app
|
||||||
|
surface (chat, suggestions, cited documents); the ONLY anonymous
|
||||||
|
content is the shared chats (the phase-16 "everything but the admin
|
||||||
|
surface is open" stance is superseded).
|
||||||
|
|
||||||
|
The deflection "Maybe try" chips (``app.rag.suggestions.
|
||||||
|
derive_suggestions``) are a SEPARATE contract (title-derived, carried
|
||||||
|
in the chat response) and are untouched by this endpoint.
|
||||||
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter, Depends
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.config import get_settings
|
from app.config import get_settings
|
||||||
|
from app.core.auth import require_user
|
||||||
|
from app.db import get_db
|
||||||
|
from app.models import SavedChat
|
||||||
from app.schemas import SuggestionList
|
from app.schemas import SuggestionList
|
||||||
|
|
||||||
router = APIRouter(tags=["chat"])
|
router = APIRouter(tags=["chat"])
|
||||||
|
|
||||||
|
|
||||||
|
def last_questions(db: Session, limit: int = 3) -> list[str]:
|
||||||
|
"""The ``limit`` most recent user questions, across all saved chats.
|
||||||
|
|
||||||
|
Chats are walked ``updated_at DESC, created_at DESC`` (the
|
||||||
|
tiebreak keeps the order deterministic when timestamps collide);
|
||||||
|
each chat's ``messages`` (a JSONB column that deserializes to a
|
||||||
|
plain Python list of ``bor.chat.v1`` dicts — NO SQL JSON ops
|
||||||
|
needed, the record shape is the ``SavedChat.messages`` model
|
||||||
|
docstring) is walked in REVERSE (conversational order is
|
||||||
|
oldest→newest), collecting the whitespace-trimmed ``text`` of
|
||||||
|
every entry with ``who == "user"``. Blank texts are skipped.
|
||||||
|
|
||||||
|
De-duplication is EXACT (case-sensitive) against the collected
|
||||||
|
window: a verbatim re-ask counts once, while a legitimately
|
||||||
|
differently-cased re-ask is kept (case-insensitive dedup would
|
||||||
|
drop it). The walk stops once ``limit`` UNIQUE texts are
|
||||||
|
collected; the result is in encounter order (newest first).
|
||||||
|
|
||||||
|
Pure-DB helper (unit-testable without the endpoint); returns
|
||||||
|
``[]`` when no saved question exists (the caller then falls back
|
||||||
|
to the seed list).
|
||||||
|
"""
|
||||||
|
result: list[str] = []
|
||||||
|
seen: set[str] = set()
|
||||||
|
for chat in db.scalars(
|
||||||
|
select(SavedChat).order_by(
|
||||||
|
SavedChat.updated_at.desc(), SavedChat.created_at.desc()
|
||||||
|
)
|
||||||
|
):
|
||||||
|
for m in reversed(chat.messages or []):
|
||||||
|
if m.get("who") != "user":
|
||||||
|
continue
|
||||||
|
question = str(m.get("text", "")).strip()
|
||||||
|
if not question or question in seen:
|
||||||
|
continue
|
||||||
|
seen.add(question)
|
||||||
|
result.append(question)
|
||||||
|
if len(result) >= limit:
|
||||||
|
return result
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
@router.get("/suggestions", response_model=SuggestionList)
|
@router.get("/suggestions", response_model=SuggestionList)
|
||||||
def suggestions() -> SuggestionList:
|
def suggestions(
|
||||||
return SuggestionList(suggestions=get_settings().suggestions)
|
_user: None = Depends(require_user), # noqa: B008 # phase 79: admin or live token
|
||||||
|
db: Session = Depends(get_db), # noqa: B008
|
||||||
|
) -> SuggestionList:
|
||||||
|
"""The onboarding chips (admin OR token user, else 401): the last 3
|
||||||
|
questions asked across saved chats — or, before any question has
|
||||||
|
ever been saved, the seed list (``BOR_SUGGESTIONS`` / the
|
||||||
|
built-in default). The deflection "Maybe try" chips are a separate
|
||||||
|
contract (``app.rag.suggestions.derive_suggestions``), untouched.
|
||||||
|
"""
|
||||||
|
qs = last_questions(db)
|
||||||
|
return SuggestionList(suggestions=qs if qs else get_settings().suggestions)
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
"""Tokens admin API (phase 79, task 02).
|
||||||
|
|
||||||
|
The admin surface for the issued access tokens (owner-locked A4):
|
||||||
|
generate a named token (the plaintext is shown **exactly once**, in the
|
||||||
|
201 body), list tokens (display fields only — no plaintext, no hashes),
|
||||||
|
and revoke one (idempotent). The whole router sits behind
|
||||||
|
:func:`app.core.auth.require_admin` (router-wide ``dependencies`` — the
|
||||||
|
:mod:`app.api.doc_drafts` pattern): tokens are admin-only, so anonymous
|
||||||
|
callers get 403 on every route — and once the token-auth login lands
|
||||||
|
(task 03), a token *user* stays 403 here too (only the admin manages
|
||||||
|
tokens).
|
||||||
|
|
||||||
|
Routes (all under ``/api`` via the ``main`` registration):
|
||||||
|
``POST /api/tokens`` (201 ``TokenCreated`` — the ONE response shape
|
||||||
|
that carries the plaintext ``token``), ``GET /api/tokens``
|
||||||
|
(``TokenList`` — newest first, secret-free), ``POST
|
||||||
|
/api/tokens/{token_id}/revoke`` (204, idempotent; unknown id → 404
|
||||||
|
``token not found``).
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, Response
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from app.core import tokens as token_service
|
||||||
|
from app.core.auth import require_admin
|
||||||
|
from app.db import get_db
|
||||||
|
from app.models import ApiToken
|
||||||
|
from app.schemas import TokenCreated, TokenCreateRequest, TokenList, TokenListItem
|
||||||
|
|
||||||
|
router = APIRouter(
|
||||||
|
prefix="/tokens",
|
||||||
|
tags=["tokens"],
|
||||||
|
dependencies=[Depends(require_admin)], # phase 79: the token admin surface is admin-only
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("", response_model=TokenCreated, status_code=201)
|
||||||
|
def create_token(
|
||||||
|
payload: TokenCreateRequest,
|
||||||
|
db: Session = Depends(get_db), # noqa: B008
|
||||||
|
) -> TokenCreated:
|
||||||
|
"""Generate one named token (201).
|
||||||
|
|
||||||
|
The ``token`` field of this response is the ONE AND ONLY moment the
|
||||||
|
plaintext exists on the wire (A4): the row stores the SHA-256 hash
|
||||||
|
of the full token string, and no other response shape — in
|
||||||
|
particular the list — ever carries it. ``label`` is the hand-out
|
||||||
|
name, display-only and NOT unique (two tokens may share a label).
|
||||||
|
Blank/over-long labels are a 422 from the schema (the house
|
||||||
|
``ValueError`` pattern — fail loud at the boundary).
|
||||||
|
"""
|
||||||
|
row, plaintext = token_service.create_token(db, payload.label)
|
||||||
|
db.commit() # the service flushes; the endpoint owns the commit
|
||||||
|
db.refresh(row) # pulls the server-default created_at
|
||||||
|
return TokenCreated(
|
||||||
|
id=row.id, label=row.label, token=plaintext, created_at=row.created_at
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("", response_model=TokenList)
|
||||||
|
def list_tokens(
|
||||||
|
db: Session = Depends(get_db), # noqa: B008
|
||||||
|
) -> TokenList:
|
||||||
|
"""All tokens, newest first (``created_at desc, id desc`` tiebreak).
|
||||||
|
|
||||||
|
Secret-free by construction: :class:`~app.schemas.TokenListItem` has
|
||||||
|
no ``token`` and no ``token_hash`` field — the list never carries a
|
||||||
|
credential in either form. ``revoked`` is derived from
|
||||||
|
``revoked_at is not None``; ``last_used_at`` stays null until the
|
||||||
|
token is first used (task 03 stamps it on ``POST /api/token-auth``)
|
||||||
|
"""
|
||||||
|
rows = (
|
||||||
|
db.execute(
|
||||||
|
select(ApiToken).order_by(ApiToken.created_at.desc(), ApiToken.id.desc())
|
||||||
|
)
|
||||||
|
.scalars()
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
items = [
|
||||||
|
TokenListItem(
|
||||||
|
id=row.id,
|
||||||
|
label=row.label,
|
||||||
|
created_at=row.created_at,
|
||||||
|
last_used_at=row.last_used_at,
|
||||||
|
revoked=row.revoked_at is not None,
|
||||||
|
)
|
||||||
|
for row in rows
|
||||||
|
]
|
||||||
|
return TokenList(tokens=items)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{token_id}/revoke", status_code=204)
|
||||||
|
def revoke_token(
|
||||||
|
token_id: uuid.UUID,
|
||||||
|
db: Session = Depends(get_db), # noqa: B008
|
||||||
|
) -> Response:
|
||||||
|
"""Revoke one token (204) — idempotent.
|
||||||
|
|
||||||
|
Already-revoked → still 204 with NO re-stamp (the original
|
||||||
|
``revoked_at`` — the revocation time — is preserved; the service
|
||||||
|
only stamps when unset). Unknown id → 404 ``token not found`` (one
|
||||||
|
message for every unknown id). Revocation takes effect immediately:
|
||||||
|
the holder's next request is refused (the task-03 live check).
|
||||||
|
"""
|
||||||
|
if not token_service.revoke(db, token_id):
|
||||||
|
raise HTTPException(status_code=404, detail="token not found")
|
||||||
|
db.commit()
|
||||||
|
return Response(status_code=204)
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user