From 872a07cee7278c1a400aaab20df0631b5d953cd1 Mon Sep 17 00:00:00 2001 From: ducoterra Date: Fri, 28 Aug 2026 12:33:18 -0400 Subject: [PATCH] =?UTF-8?q?feat(ui):=20rename=20nav=20items=20=E2=80=94=20?= =?UTF-8?q?"Sources"=20becomes=20"RAG",=20"Git=20sources"=20becomes=20"Sou?= =?UTF-8?q?rces"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Owner request (2026-08-28): the two admin-only nav items read like the same thing, so they are relabeled — the document-catalog link (#nav-sources, /sources.html) becomes "RAG" and the source-manager link (#nav-git-sources, /git-sources.html) becomes "Sources". Phase 48 (48_nav_rename_sources), label-only per the locked decision: - all six pages (index, sources, git-sources, tuning, document, login): the two texts swap; ids, hrefs, hidden defaults, is-active / aria-current placement, and nav order (Chat, RAG, Sources, Tuning) are byte-unchanged otherwise. - header.js: comment/docstring label mentions only — the reveal-by-id logic is untouched (ship-hidden/reveal contract intact). - test_git_sources_admin.py / test_mobile_hamburger_nav.py: the two suites that asserted the old label text are updated; comment-only label fixes in test_shared_header.py / test_nav_consistency.py. - tests/e2e/test_nav_rename_sources.py: the story E2E (green in isolation) — renamed labels + unchanged hrefs/order/markers on all six pages, click navigation with the active marker, the anonymous ship-hidden contract, and regression guards for the untouched controls (#sync-label "Sync sources", viewer #doc-back "Sources"). - All eight surrounding header/nav suites stay green in isolation; unit+integration green, app/ coverage 99% (frontend-only change), ruff + pyright clean. Note: per this phase file-level staging, the six page files and header.js also carry the same-day in-flight owner rework that was already in the working tree when phase 48 ran (mobile sign-in dropdown copy, sync button ship-hidden on the Sources page); the label rename itself is the two-text swap on each page. --- .../48_nav_rename_sources/00_phase.md | 40 ++ .../01_nav_label_swap.md | 46 ++ .../02_existing_test_labels.md | 34 ++ .../03_story_e2e_regression_commit.md | 33 ++ .agent/user_stories/nav-sources-rag-rename.md | 49 +++ frontend/assets/header.js | 17 +- frontend/document.html | 14 +- frontend/git-sources.html | 12 +- frontend/index.html | 12 +- frontend/login.html | 12 +- frontend/sources.html | 16 +- frontend/tuning.html | 12 +- tests/e2e/test_git_sources_admin.py | 6 +- tests/e2e/test_mobile_hamburger_nav.py | 29 +- tests/e2e/test_nav_consistency.py | 80 ++-- tests/e2e/test_nav_rename_sources.py | 400 ++++++++++++++++++ tests/e2e/test_shared_header.py | 91 ++-- 17 files changed, 785 insertions(+), 118 deletions(-) create mode 100644 .agent/phases/complete/48_nav_rename_sources/00_phase.md create mode 100644 .agent/phases/complete/48_nav_rename_sources/01_nav_label_swap.md create mode 100644 .agent/phases/complete/48_nav_rename_sources/02_existing_test_labels.md create mode 100644 .agent/phases/complete/48_nav_rename_sources/03_story_e2e_regression_commit.md create mode 100644 .agent/user_stories/nav-sources-rag-rename.md create mode 100644 tests/e2e/test_nav_rename_sources.py diff --git a/.agent/phases/complete/48_nav_rename_sources/00_phase.md b/.agent/phases/complete/48_nav_rename_sources/00_phase.md new file mode 100644 index 0000000..ed53c22 --- /dev/null +++ b/.agent/phases/complete/48_nav_rename_sources/00_phase.md @@ -0,0 +1,40 @@ +# Phase 48 — Nav rename: "Sources" → "RAG", "Git sources" → "Sources" + +**Source:** owner request (chat, 2026-08-28) — "The 'Git sources' navbar item should be renamed to 'Sources' and the 'Sources' navbar item should be renamed to 'RAG'." +**Story:** `.agent/user_stories/nav-sources-rag-rename.md` +**Context:** the shared header (phase 19, `frontend/assets/header.js`) ships ONE nav on all six pages — `[Chat, #nav-sources "Sources" → /sources.html, #nav-git-sources "Git sources" → /git-sources.html, #nav-tuning "Tuning"]` — with the two admin-only links ship-hidden and revealed by id once `/api/whoami` says admin (phase 16 contract, phase 34 same-header-everywhere, phase 46 mobile hamburger). Both labels are plain `` text in the six page files; `header.js` toggles only the `hidden` attribute and never reads the label, so the rename is markup-only. + +## Objective +Relabel the two admin-only nav items across all six pages — the document-catalog link (`#nav-sources`) becomes **"RAG"** and the source-manager link (`#nav-git-sources`) becomes **"Sources"** — with ids, hrefs, order, the ship-hidden/reveal contract, and every other page control (viewer back button, Sync button, page titles/h1s) unchanged. + +## Dependencies +- `34_consistent_navbar` / `19_shared_header` (complete) — the same full header block on all six pages and the `header.js` reveal-by-id contract this phase preserves. +- `35_git_sources_admin` (complete) — the `#nav-git-sources` link (and the `/git-sources.html` page) it labels. +- `46_mobile_hamburger_nav` (complete) — the `LINK_TEXTS` assertion in `test_mobile_hamburger_nav.py` that this phase updates. + +## Tasks +1. `01_nav_label_swap.md` — swap the two label texts in all six page files + the label mentions in `header.js`'s docstring/comments (no logic). +2. `02_existing_test_labels.md` — update the two test suites that assert the old label text; leave every other assertion (viewer back button, sync label, h1/title markers) untouched. +3. `03_story_e2e_regression_commit.md` — the story E2E (`test_nav_rename_sources.py`), the regression suites in isolation, ruff + pyright, the one `--no-gpg-sign` commit, and the phase-dir move. + +## Testing & Quality +- Unit/integration: no new backend logic (frontend markup only). The existing no-CDN integration test (`tests/integration/test_api.py::test_html_pages_served_locally_no_cdn`) must still pass — the swap touches no tags or origins (the `/git-sources.html` "Git sources" marker keeps matching the page's unchanged `

`). +- Coverage: frontend-only; the `app/` >90% gate is unaffected (unchanged). +- E2E (mandatory, A16): `tests/e2e/test_nav_rename_sources.py` — the story gate, run in isolation. + +## Completion Criteria +- [ ] All six pages show **"RAG"** on `#nav-sources` (href `/sources.html`) and **"Sources"** on `#nav-git-sources` (href `/git-sources.html`); nav order and `is-active`/`aria-current` placement unchanged; no id/href/`hidden` change. +- [ ] Anonymous still sees neither link; the admin sees both (contract preserved, proven by the story E2E + `test_shared_header.py`). +- [ ] `uv run pytest` green (the two updated suites included); `uv run pytest --cov=app --cov-report=term-missing` TOTAL unchanged (no backend change). +- [ ] Regression E2E suites green in isolation: `test_nav_consistency.py`, `test_shared_header.py`, `test_mobile_hamburger_nav.py`, `test_git_sources_admin.py`, `test_header_consistency.py`, `test_sources_midstream_bug.py`, `test_smoke.py`. +- [ ] `uv run pytest tests/e2e/test_nav_rename_sources.py -v --no-cov` green in isolation. +- [ ] `uv run ruff check . && uv run pyright` clean (no Python behavior change, but run the gate). +- [ ] UI Structure Check (AGENTS.md rule 5): labeled links, landmarks/contrast/focus unchanged; no CDN (rule 6). +- [ ] One `--no-gpg-sign` commit staging only this phase's files; `.agent/phases/todo/48_nav_rename_sources/` moved to `.agent/phases/complete/`. + +## Locked decisions +- **Label-only rename (owner request 2026-08-28)** — exactly the two nav item texts change. Element ids (`#nav-sources`, `#nav-git-sources`), hrefs, nav order, the ship-hidden/reveal contract, `header.js` behavior, and every other label (the document viewer's "Sources" back button, the "Sync sources" button, page ``/`<h1>` — incl. "Knowledge base" and "Git sources") are OUT of scope. If the owner later wants the page titles/h1s to follow the nav labels, that is a follow-up phase, not this one. +- **A11 untouched** — vanilla HTML only, no CDN, no new packages, no new tags. +- **A10 untouched** — no endpoint, auth, or `header.js` logic change; the rename rides the existing reveal-by-id path. +- **No schema / migration** — purely a markup + test-label change. +- **A16 / A17 honoured** — one new story E2E suite + one atomic `--no-gpg-sign` commit. diff --git a/.agent/phases/complete/48_nav_rename_sources/01_nav_label_swap.md b/.agent/phases/complete/48_nav_rename_sources/01_nav_label_swap.md new file mode 100644 index 0000000..794ddbf --- /dev/null +++ b/.agent/phases/complete/48_nav_rename_sources/01_nav_label_swap.md @@ -0,0 +1,46 @@ +# Task 01 — Swap the two nav label texts on all six pages + +**Phase:** `48_nav_rename_sources` · **Story:** `.agent/user_stories/nav-sources-rag-rename.md` + +## Objective +Rename the nav items themselves: `#nav-sources` ("Sources" → **"RAG"**) and `#nav-git-sources` ("Git sources" → **"Sources"") in the shared header markup of all six pages, plus the label mentions in `header.js`'s doc comments. Markup only — no ids, hrefs, classes, logic, or other text changes. + +## Work +On **each** of the six page files — `frontend/index.html`, `frontend/sources.html`, `frontend/git-sources.html`, `frontend/tuning.html`, `frontend/document.html`, `frontend/login.html` — make exactly two text swaps inside `<nav class="app-nav" aria-label="Primary">`: + +1. The catalog link — change the visible text only: + ```html + <a href="/sources.html" class="nav-link" id="nav-sources" hidden>Sources</a> + ``` + becomes + ```html + <a href="/sources.html" class="nav-link" id="nav-sources" hidden>RAG</a> + ``` + (On `sources.html` the link carries `class="nav-link is-active" aria-current="page"` — keep those attributes exactly as they are; swap only `Sources` → `RAG`.) + +2. The manager link — change the visible text only: + ```html + <a href="/git-sources.html" class="nav-link" id="nav-git-sources" hidden>Git sources</a> + ``` + becomes + ```html + <a href="/git-sources.html" class="nav-link" id="nav-git-sources" hidden>Sources</a> + ``` + (On `git-sources.html` the link carries `class="nav-link is-active" aria-current="page"` — keep them; swap only `Git sources` → `Sources`.) + +3. `frontend/assets/header.js` — **comments only** (the reveal logic is id-based and must stay byte-identical): in the module docstring (the nav-link inventory: `"Sources" (#nav-sources, phase 19), "Git sources" (#nav-git-sources, phase 35)`) and in the inline comment above the `navGitSources` reveal, update the quoted labels to the new ones (`"RAG" (#nav-sources)`, `"Sources" (#nav-git-sources)`). No executable line of `header.js` changes. + +Rules for all edits: +- Do **not** touch element ids, `href`s, `hidden` defaults, `class` attributes, `aria-current`, indentation, or any other text on the page (page `<title>`, `<h1>`, the viewer's "Sources" back button, the "Sync sources" button label — all stay). +- Do **not** add `is-active` anywhere or move the links — the physical nav order stays Chat, `#nav-sources`, `#nav-git-sources`, `#nav-tuning` (the labels just swap, so it now *reads* Chat, RAG, Sources, Tuning). +- The phase-16/19/35 ship-hidden contract is untouched: the links still ship `hidden` and `header.js` still reveals them by id for admins. + +## Testing & Quality +- No new unit/integration logic (frontend markup only). +- Coverage: frontend-only; `app/` coverage unaffected. +- The no-CDN integration test (`tests/integration/test_api.py::test_html_pages_served_locally_no_cdn`) still passes — no tags or origins change; its `("/git-sources.html", "Git sources")` marker still matches that page's unchanged `<h1>Git sources</h1>`. + +## Completion Criteria +- [ ] All six pages: `#nav-sources` renders text **RAG** and `#nav-git-sources` renders text **Sources**; ids/hrefs/`hidden`/`is-active` unchanged (a quick `grep -c 'id="nav-sources" hidden>RAG<'` per page returns 1, and `grep -c 'id="nav-git-sources" .*>Sources<'` returns 1). +- [ ] `git diff --stat` shows only the six HTML files + `frontend/assets/header.js` (comment-only) changed. +- [ ] `uv run pytest tests/unit tests/integration -q` green; `uv run ruff check . && uv run pyright` clean. diff --git a/.agent/phases/complete/48_nav_rename_sources/02_existing_test_labels.md b/.agent/phases/complete/48_nav_rename_sources/02_existing_test_labels.md new file mode 100644 index 0000000..513c529 --- /dev/null +++ b/.agent/phases/complete/48_nav_rename_sources/02_existing_test_labels.md @@ -0,0 +1,34 @@ +# Task 02 — Update the tests that assert the old nav label text + +**Phase:** `48_nav_rename_sources` · **Story:** `.agent/user_stories/nav-sources-rag-rename.md` + +## Objective +Keep the existing suites honest after the rename: update the (few) assertions and label-describing comments that reference the OLD label texts, without touching any assertion that is about a different control. + +## Work +Only these tests assert the old label text (verified by grepping `tests/` for the strings) — update exactly these: + +1. `tests/e2e/test_git_sources_admin.py` — `test_admin_nav_link_on_all_five_pages_and_click_navigates`: + - the comment `pointing at the git sources page, labeled "Git sources" — and it` → `labeled "Sources"`; + - `expect(link).to_have_text("Git sources")` → `expect(link).to_have_text("Sources")`. + - This suite has no assertion on `#nav-sources`'s TEXT (it is id-based everywhere) — do not add one here; the story E2E (task 03) owns the label checks. +2. `tests/e2e/test_mobile_hamburger_nav.py` — + - `LINK_TEXTS = ("Chat", "Sources", "Git sources", "Tuning")` → `LINK_TEXTS = ("Chat", "RAG", "Sources", "Tuning")` (DOM order unchanged — only the labels swapped); + - the comment near `test_…` that reads `Chat / Sources / Git sources / Tuning — i.e. the whoami reveal` → `Chat / RAG / Sources / Tuning …`. +3. **Comments-only drift fixes** (no assertion changes) where a suite's docstring/comment quotes the old labels as the link's identity: `tests/e2e/test_shared_header.py` (module docstring line `"the 'Sources' nav link (#nav-sources)"` → `'RAG'`), `tests/e2e/test_nav_consistency.py` (the comment `the Git sources link joined in phase 35` → mention the relabeled link), `tests/e2e/test_header_consistency.py` if it quotes the labels. Where a comment also explains the phase-35 origin, keep that history and only fix the quoted label. + +**Explicitly DO NOT touch** (different controls / different text — they stay green and must stay): +- `tests/e2e/test_nav_consistency.py` lines asserting the document viewer's **back button** `span` text "Sources" / "Chat" — that is the viewer's back link (`document.js`), not the nav. +- `tests/e2e/test_document_back_navigation.py` — same back button. +- `tests/unit/test_document_viewer.py` — the `'Sources' in js` check on the viewer's back-link label. +- `tests/e2e/test_nav_consistency.py` `expect(page.locator("#sync-label")).to_have_text("Sync sources")` — the Sync button label is unchanged. +- `tests/integration/test_api.py::test_html_pages_served_locally_no_cdn` — the `("/git-sources.html", "Git sources")` marker matches the page's unchanged `<h1>`; leave it. + +## Testing & Quality +- No new logic — assertion-text updates only. +- Coverage: frontend/test-only; `app/` coverage unaffected. + +## Completion Criteria +- [ ] `grep -rn '"Git sources"\|Git sources' tests/e2e/*.py` shows no remaining assertion expecting the old nav label (only phase-history comments, if any, are acceptable — and none should quote it as the current label). +- [ ] `uv run pytest tests/unit tests/integration -q` green; `uv run ruff check . && uv run pyright` clean. +- [ ] The two updated E2E suites pass in isolation: `uv run pytest tests/e2e/test_git_sources_admin.py -v --no-cov` and `uv run pytest tests/e2e/test_mobile_hamburger_nav.py -v --no-cov` (DB up). diff --git a/.agent/phases/complete/48_nav_rename_sources/03_story_e2e_regression_commit.md b/.agent/phases/complete/48_nav_rename_sources/03_story_e2e_regression_commit.md new file mode 100644 index 0000000..11b4504 --- /dev/null +++ b/.agent/phases/complete/48_nav_rename_sources/03_story_e2e_regression_commit.md @@ -0,0 +1,33 @@ +# Task 03 — Story E2E, regression suites, commit, phase move + +**Phase:** `48_nav_rename_sources` · **Story:** `.agent/user_stories/nav-sources-rag-rename.md` + +## Objective +Prove the renamed nav end to end with the story's dedicated Playwright suite (run in isolation per A16), confirm the surrounding header suites stay green, and land the one atomic `--no-gpg-sign` commit. + +## Work +1. **Create `tests/e2e/test_nav_rename_sources.py`** (one story, one file — follow the conventions of `test_git_sources_admin.py` / `test_nav_consistency.py`: the shared `app_url` / `db_ready` fixtures, the `login(page, app_url, next=…)` helper, desktop viewport 1280×800, `expect` sync-API assertions, a module docstring stating the story + the rename). The six pages under test are `/` (chat), `/sources.html`, `/git-sources.html`, `/tuning.html`, `/login.html`, and `/document.html` (viewer — seed one document row first, reusing the seeding pattern `test_nav_consistency.py` uses for its viewer pass, and wait for `#doc-title` to settle as that file does). + + Test cases: + - **admin_labels_on_all_six_pages** — signed-in admin visits each of the six pages; on each: `#nav-sources` is visible, has text exactly `RAG` and href `/sources.html`; `#nav-git-sources` is visible, has text exactly `Sources` and href `/git-sources.html`; the nav DOM order is Chat, RAG, Sources, Tuning (assert the `.app-nav a` text sequence); the current page's link carries `is-active`/`aria-current="page"` (and only it does). + - **click_navigates_with_marker** — from the chat page: click `#nav-sources` (label "RAG") → lands on `/sources.html` where `#nav-sources` is the active link; from the chat page: click `#nav-git-sources` (label "Sources") → lands on `/git-sources.html` where `#nav-git-sources` is the active link. + - **anonymous_sees_neither** — anonymous (no login): on `/` and `/login.html` both `#nav-sources` and `#nav-git-sources` are present in the DOM but hidden (ship-hidden contract unchanged), `#sign-in-link` visible. + - **untouched_controls_stay** — the rename did not leak: on `/sources.html` the Sync button still reads "Sync sources" (`#sync-label`), and on the settled viewer page the back button's span still reads "Sources" (the viewer back link is a different control — regression guard for task 02's do-not-touch list). +2. **Run the regression suites in isolation** (DB up, `--no-cov`), fixing nothing unless a test genuinely asserted a renamed nav label (if one does, update it as in task 02 and note it in the commit message): + `test_nav_consistency.py`, `test_shared_header.py`, `test_mobile_hamburger_nav.py`, `test_git_sources_admin.py`, `test_header_consistency.py`, `test_sources_midstream_bug.py`, `test_smoke.py`, `test_tuning_nav_link.py`. +3. **Full gates**: `uv run pytest` (unit + integration) green; `uv run pytest --cov=app --cov-report=term-missing` with TOTAL unchanged from the pre-phase baseline (frontend-only change); `uv run ruff check . && uv run pyright` clean. +4. **UI Structure Check (AGENTS.md rule 5)** — the relabeled links are still labeled `<a>`s inside the semantic `<nav aria-label="Primary">`; landmarks/contrast/focus-visible untouched (label text only); no CDN (rule 6 — the no-CDN integration test covers it). +5. **Commit** — one atomic commit staging exactly this phase's files (the six HTML files, `frontend/assets/header.js`, `tests/e2e/test_git_sources_admin.py`, `tests/e2e/test_mobile_hamburger_nav.py`, the comment-only test files from task 02, and the new `tests/e2e/test_nav_rename_sources.py`): + `feat(ui): rename nav items — "Sources" becomes "RAG", "Git sources" becomes "Sources"` with a body citing the owner request (2026-08-28) + phase 48. `git commit --no-gpg-sign` (repo also has `commit.gpgsign=false`). +6. **Move the phase directory**: `mv .agent/phases/todo/48_nav_rename_sources .agent/phases/complete/` and `git add -f .agent/phases/complete/48_nav_rename_sources` into the SAME commit as step 5 (`.agent/` is gitignored by design — AGENTS.md rule 8), plus the story file `git add -f .agent/user_stories/nav-sources-rag-rename.md`. + +## Testing & Quality +- Story E2E: `tests/e2e/test_nav_rename_sources.py` green **in isolation**. +- Coverage: frontend-only; `app/` >90% gate unaffected (TOTAL unchanged). + +## Completion Criteria +- [ ] `uv run pytest tests/e2e/test_nav_rename_sources.py -v --no-cov` green in isolation (DB up). +- [ ] All eight regression suites green in isolation (commands above). +- [ ] `uv run pytest` green; `uv run pytest --cov=app --cov-report=term-missing` TOTAL unchanged; `uv run ruff check . && uv run pyright` clean. +- [ ] One `--no-gpg-sign` commit containing the code/test files, the story file, and the moved phase directory; `.agent/phases/todo/` no longer lists 48. +- [ ] No behavior change in completed phases (the suites above are the proof). diff --git a/.agent/user_stories/nav-sources-rag-rename.md b/.agent/user_stories/nav-sources-rag-rename.md new file mode 100644 index 0000000..fdeb216 --- /dev/null +++ b/.agent/user_stories/nav-sources-rag-rename.md @@ -0,0 +1,49 @@ +# Story: Nav rename — "Sources" becomes "RAG", "Git sources" becomes "Sources" + +**Phase:** `48_nav_rename_sources` · **E2E:** `tests/e2e/test_nav_rename_sources.py` + +## Narrative + +As **the admin (owner)**, the two admin-only nav items are confusing: +"Sources" (the document catalog) and "Git sources" (the source manager) +read like the same thing. I want the navbar to say **"RAG"** for the +document catalog page and **"Sources"** for the source manager page, so +the labels say what each page *is*: RAG = the indexed knowledge base you +chat against; Sources = where that knowledge comes from. + +- **Given** I am signed in as admin, on any of the six pages (chat, + sources, git-sources, tuning, document viewer, login) +- **When** I look at the primary nav +- **Then** the catalog link (`#nav-sources`, → `/sources.html`) is + labeled **"RAG"** and the manager link (`#nav-git-sources`, → + `/git-sources.html`) is labeled **"Sources"** — same ids, same hrefs, + same order, same admin-only ship-hidden/reveal contract as before. + +## Acceptance criteria +1. All six pages (`frontend/index.html`, `sources.html`, + `git-sources.html`, `tuning.html`, `document.html`, `login.html`) + carry the swapped label text on `#nav-sources` ("RAG") and + `#nav-git-sources` ("Sources"). Element ids, `href`s, `hidden` + defaults, `is-active`/`aria-current` markers, and the nav order + (Chat, RAG, Sources, Tuning) are unchanged — this is a label-only + rename, and `header.js` logic (reveal by id) is untouched. +2. Anonymous visitors still see neither link (ship-hidden contract + unchanged); the admin sees both, as before. +3. Existing test assertions on the old label text are updated + (`test_git_sources_admin.py` nav-link text, + `test_mobile_hamburger_nav.py` `LINK_TEXTS`); assertions that are + NOT about these two nav items stay untouched (the document viewer's + "Sources" back button, the "Sync sources" button label, the + `/git-sources.html` page-`<h1>`/`<title>` marker). +4. Story E2E `tests/e2e/test_nav_rename_sources.py` proves: admin sees + "RAG" (→ `/sources.html`) and "Sources" (→ `/git-sources.html`) on + all six pages; clicking each lands on the right page with + `is-active`; anonymous sees both hidden. +5. Frontend-only: unit + integration green, `app/` coverage + unaffected, ruff + pyright clean, no CDN, one `--no-gpg-sign` commit. + +## Playwright Mapping Rule +`tests/e2e/test_nav_rename_sources.py` — one story, one file, run in +isolation: the two relabeled nav items' full lifecycle (labels, hrefs, +admin reveal, anonymous hide, click-navigation with the current-page +marker) on the shared header, desktop viewport. diff --git a/frontend/assets/header.js b/frontend/assets/header.js index 34f2870..f706ac9 100644 --- a/frontend/assets/header.js +++ b/frontend/assets/header.js @@ -6,8 +6,8 @@ * * • the Sign in / Sign out auth pair (phase 16, exactly one visible — * decided by /api/whoami at load); - * • the admin-only nav links — "Sources" (#nav-sources, phase 19), - * "Git sources" (#nav-git-sources, phase 35) and "Tuning" + * • the admin-only nav links — "RAG" (#nav-sources, phase 19), + * "Sources" (#nav-git-sources, phase 35) and "Tuning" * (#nav-tuning, phase 29) — phase 19 UX revision * (owner permission 2026-08-23): hidden for anonymous on EVERY * page, revealed for admin. Phase 34 task 03 (owner confirmation @@ -106,17 +106,16 @@ export async function initSharedHeader() { // a query-safe "/…" string (never "//"; ? # and spaces stay // percent-encoded in it), so it rides in next= as-is — the same shape // the static fallbacks use (login.js safeNext re-validates it). - const signIn = document.querySelector("#sign-in-link"); - if (signIn) { - signIn.hidden = admin; - signIn.href = "/login.html?next=" + (window.location.pathname || "/"); - } + document.querySelectorAll(".sign-in-link").forEach(link => { + link.hidden = admin; + link.href = "/login.html?next=" + (window.location.pathname || "/"); + }); document.querySelectorAll(".sign-out-btn").forEach(btn => { btn.hidden = !admin; }); const navSources = document.querySelector("#nav-sources"); if (navSources) navSources.hidden = !admin; - // Phase 35 (owner permission 2026-08-26): the Git sources nav link — + // Phase 35 (owner permission 2026-08-26): the Sources nav link — // admin-only, the same ship-hidden / reveal-for-admin contract as - // the Sources link above. + // the RAG link above. const navGitSources = document.querySelector("#nav-git-sources"); if (navGitSources) navGitSources.hidden = !admin; // Phase 29: the Global Tuning nav link (every page from phase 34 diff --git a/frontend/document.html b/frontend/document.html index fe41046..ede9935 100644 --- a/frontend/document.html +++ b/frontend/document.html @@ -41,19 +41,25 @@ permission 2026-08-23) — hidden by default, header.js reveals it once whoami says admin. The soft-gated page itself is unchanged. --> - <a href="/sources.html" class="nav-link" id="nav-sources" hidden>Sources</a> + <a href="/sources.html" class="nav-link" id="nav-sources" hidden>RAG</a> <!-- Phase 35 (owner permission 2026-08-26): the Git sources link is admin-only — hidden by default, header.js reveals it once whoami says admin, exactly like the Sources link above. --> - <a href="/git-sources.html" class="nav-link" id="nav-git-sources" hidden>Git sources</a> + <a href="/git-sources.html" class="nav-link" id="nav-git-sources" hidden>Sources</a> <!-- Phase 29 (now every page — phase 34, owner confirmation 2026-08-26): the Global Tuning link is admin-only (owner permission 2026-08-25) — hidden by default, header.js reveals it once whoami says admin, exactly like the Sources link above. --> <a href="/tuning.html" class="nav-link" id="nav-tuning" hidden>Tuning</a> - <!-- Phase 46 (mobile dropdown copy — desktop bar copy is + <!-- Phase 46 (mobile dropdown copy: sign-in — desktop bar copy is + outside the nav; see styles.css .sign-in-mobile rules). --> + <a href="/login.html?next=/" class="auth-link sign-in-link sign-in-mobile" id="sign-in-link-mobile" hidden> + <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M10 4h8a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-8"/><path d="M4 12h11"/><path d="m12 9 3 3-3 3"/></svg> + <span class="auth-label">Sign in</span> + </a> + <!-- Phase 46 (mobile dropdown copy — desktop bar copy is outside the nav; see styles.css .sign-out-mobile rules). --> <button type="button" class="auth-link sign-out-btn sign-out-mobile" id="sign-out-btn-mobile" aria-label="Sign out" hidden> <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M14 4H6a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8"/><path d="M9 12h11"/><path d="m17 9 3 3-3 3"/></svg> @@ -70,7 +76,7 @@ out is visible; /api/whoami decides at load (the shared header module). Icon-only below 640px (aria-labels keep the accessible names). --> - <a href="/login.html?next=/document.html" class="auth-link" id="sign-in-link" hidden> + <a href="/login.html?next=/document.html" class="auth-link sign-in-link" id="sign-in-link" hidden> <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M10 4h8a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-8"/><path d="M4 12h11"/><path d="m12 9 3 3-3 3"/></svg> <span class="auth-label">Sign in</span> </a> diff --git a/frontend/git-sources.html b/frontend/git-sources.html index 9d9c025..7dd59f4 100644 --- a/frontend/git-sources.html +++ b/frontend/git-sources.html @@ -37,19 +37,25 @@ 2026-08-26): the Sources link is admin-only (owner permission 2026-08-23) — hidden by default, header.js reveals it once whoami says admin. --> - <a href="/sources.html" class="nav-link" id="nav-sources" hidden>Sources</a> + <a href="/sources.html" class="nav-link" id="nav-sources" hidden>RAG</a> <!-- Phase 35 (owner permission 2026-08-26): the Git sources link is admin-only — hidden by default, header.js reveals it once whoami says admin, exactly like the Sources link above; this page IS the current one, so the link carries is-active + aria-current like Tuning on tuning.html. --> - <a href="/git-sources.html" class="nav-link is-active" aria-current="page" id="nav-git-sources" hidden>Git sources</a> + <a href="/git-sources.html" class="nav-link is-active" aria-current="page" id="nav-git-sources" hidden>Sources</a> <!-- Phase 29 (now every page — phase 34, owner confirmation 2026-08-26): the Global Tuning link is admin-only (owner permission 2026-08-25) — hidden by default, header.js reveals it once whoami says admin. --> <a href="/tuning.html" class="nav-link" id="nav-tuning" hidden>Tuning</a> + <!-- Phase 46 (mobile dropdown copy: sign-in — desktop bar copy is + outside the nav; see styles.css .sign-in-mobile rules). --> + <a href="/login.html?next=/" class="auth-link sign-in-link sign-in-mobile" id="sign-in-link-mobile" hidden> + <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M10 4h8a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-8"/><path d="M4 12h11"/><path d="m12 9 3 3-3 3"/></svg> + <span class="auth-label">Sign in</span> + </a> <!-- Phase 46 (mobile dropdown copy — desktop bar copy is outside the nav; see styles.css .sign-out-mobile rules). --> <button type="button" class="auth-link sign-out-btn sign-out-mobile" id="sign-out-btn-mobile" aria-label="Sign out" hidden> @@ -67,7 +73,7 @@ out is visible; /api/whoami decides at load (the shared header module). Icon-only below 640px (aria-labels keep the accessible names). --> - <a href="/login.html?next=/git-sources.html" class="auth-link" id="sign-in-link" hidden> + <a href="/login.html?next=/git-sources.html" class="auth-link sign-in-link" id="sign-in-link" hidden> <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M10 4h8a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-8"/><path d="M4 12h11"/><path d="m12 9 3 3-3 3"/></svg> <span class="auth-label">Sign in</span> </a> diff --git a/frontend/index.html b/frontend/index.html index 9dd6854..5b85193 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -31,18 +31,24 @@ permission 2026-08-23) — hidden by default, header.js reveals it once whoami says admin. The soft-gated page itself is unchanged. --> - <a href="/sources.html" class="nav-link" id="nav-sources" hidden>Sources</a> + <a href="/sources.html" class="nav-link" id="nav-sources" hidden>RAG</a> <!-- Phase 35 (owner permission 2026-08-26): the Git sources link is admin-only — hidden by default, header.js reveals it once whoami says admin, exactly like the Sources link above. --> - <a href="/git-sources.html" class="nav-link" id="nav-git-sources" hidden>Git sources</a> + <a href="/git-sources.html" class="nav-link" id="nav-git-sources" hidden>Sources</a> <!-- Phase 29 (now every page — phase 34, owner confirmation 2026-08-26): the Global Tuning link is admin-only (owner permission 2026-08-25) — hidden by default, header.js reveals it once whoami says admin, exactly like the Sources link above. --> <a href="/tuning.html" class="nav-link" id="nav-tuning" hidden>Tuning</a> + <!-- Phase 46 (mobile dropdown copy: sign-in — desktop bar copy is + outside the nav; see styles.css .sign-in-mobile rules). --> + <a href="/login.html?next=/" class="auth-link sign-in-link sign-in-mobile" id="sign-in-link-mobile" hidden> + <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M10 4h8a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-8"/><path d="M4 12h11"/><path d="m12 9 3 3-3 3"/></svg> + <span class="auth-label">Sign in</span> + </a> <!-- Phase 46 (mobile dropdown copy — desktop bar copy is outside the nav; see styles.css .sign-out-mobile rules). --> <button type="button" class="auth-link sign-out-btn sign-out-mobile" id="sign-out-btn-mobile" aria-label="Sign out" hidden> @@ -60,7 +66,7 @@ out is visible; /api/whoami decides at load (the shared header module). Icon-only below 640px (aria-labels keep the accessible names). --> - <a href="/login.html?next=/sources.html" class="auth-link" id="sign-in-link" hidden> + <a href="/login.html?next=/sources.html" class="auth-link sign-in-link" id="sign-in-link" hidden> <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M10 4h8a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-8"/><path d="M4 12h11"/><path d="m12 9 3 3-3 3"/></svg> <span class="auth-label">Sign in</span> </a> diff --git a/frontend/login.html b/frontend/login.html index 4b83256..a8f1e36 100644 --- a/frontend/login.html +++ b/frontend/login.html @@ -32,20 +32,26 @@ permission 2026-08-23) — hidden by default, header.js reveals it once whoami says admin. The soft-gated page itself is unchanged. --> - <a href="/sources.html" class="nav-link" id="nav-sources" hidden>Sources</a> + <a href="/sources.html" class="nav-link" id="nav-sources" hidden>RAG</a> <!-- Phase 35 (owner permission 2026-08-26): the Git sources link is admin-only — hidden by default, header.js reveals it once whoami says admin, exactly like the Sources link above. The phase-34 identical-header contract requires it here too: every page's nav carries the same four links (Chat, Sources, Git sources, Tuning). --> - <a href="/git-sources.html" class="nav-link" id="nav-git-sources" hidden>Git sources</a> + <a href="/git-sources.html" class="nav-link" id="nav-git-sources" hidden>Sources</a> <!-- Phase 29 (now every page — phase 34, owner confirmation 2026-08-26): the Global Tuning link is admin-only (owner permission 2026-08-25) — hidden by default, header.js reveals it once whoami says admin, exactly like the Sources link above. --> <a href="/tuning.html" class="nav-link" id="nav-tuning" hidden>Tuning</a> + <!-- Phase 46 (mobile dropdown copy: sign-in — desktop bar copy is + outside the nav; see styles.css .sign-in-mobile rules). --> + <a href="/login.html?next=/" class="auth-link sign-in-link sign-in-mobile" id="sign-in-link-mobile" hidden> + <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M10 4h8a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-8"/><path d="M4 12h11"/><path d="m12 9 3 3-3 3"/></svg> + <span class="auth-label">Sign in</span> + </a> <!-- Phase 46 (mobile dropdown copy — desktop bar copy is outside the nav; see styles.css .sign-out-mobile rules). --> <button type="button" class="auth-link sign-out-btn sign-out-mobile" id="sign-out-btn-mobile" aria-label="Sign out" hidden> @@ -63,7 +69,7 @@ out is visible; /api/whoami decides at load (the shared header module). Icon-only below 640px (aria-labels keep the accessible names). --> - <a href="/login.html?next=/login.html" class="auth-link" id="sign-in-link" hidden> + <a href="/login.html?next=/login.html" class="auth-link sign-in-link" id="sign-in-link" hidden> <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M10 4h8a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-8"/><path d="M4 12h11"/><path d="m12 9 3 3-3 3"/></svg> <span class="auth-label">Sign in</span> </a> diff --git a/frontend/sources.html b/frontend/sources.html index ea509bc..1510845 100644 --- a/frontend/sources.html +++ b/frontend/sources.html @@ -31,18 +31,24 @@ permission 2026-08-23) — hidden by default, header.js reveals it once whoami says admin. The soft-gated page itself is unchanged. --> - <a href="/sources.html" class="nav-link is-active" aria-current="page" id="nav-sources" hidden>Sources</a> + <a href="/sources.html" class="nav-link is-active" aria-current="page" id="nav-sources" hidden>RAG</a> <!-- Phase 35 (owner permission 2026-08-26): the Git sources link is admin-only — hidden by default, header.js reveals it once whoami says admin, exactly like the Sources link above. --> - <a href="/git-sources.html" class="nav-link" id="nav-git-sources" hidden>Git sources</a> + <a href="/git-sources.html" class="nav-link" id="nav-git-sources" hidden>Sources</a> <!-- Phase 29 (now every page — phase 34, owner confirmation 2026-08-26): the Global Tuning link is admin-only (owner permission 2026-08-25) — hidden by default, header.js reveals it once whoami says admin, exactly like the Sources link above. --> <a href="/tuning.html" class="nav-link" id="nav-tuning" hidden>Tuning</a> + <!-- Phase 46 (mobile dropdown copy: sign-in — desktop bar copy is + outside the nav; see styles.css .sign-in-mobile rules). --> + <a href="/login.html?next=/" class="auth-link sign-in-link sign-in-mobile" id="sign-in-link-mobile" hidden> + <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M10 4h8a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-8"/><path d="M4 12h11"/><path d="m12 9 3 3-3 3"/></svg> + <span class="auth-label">Sign in</span> + </a> <!-- Phase 46 (mobile dropdown copy — desktop bar copy is outside the nav; see styles.css .sign-out-mobile rules). --> <button type="button" class="auth-link sign-out-btn sign-out-mobile" id="sign-out-btn-mobile" aria-label="Sign out" hidden> @@ -60,7 +66,7 @@ out is visible; /api/whoami decides at load (the shared header module). Icon-only below 640px (aria-labels keep the accessible names). --> - <a href="/login.html?next=/sources.html" class="auth-link" id="sign-in-link" hidden> + <a href="/login.html?next=/sources.html" class="auth-link sign-in-link" id="sign-in-link" hidden> <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M10 4h8a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-8"/><path d="M4 12h11"/><path d="m12 9 3 3-3 3"/></svg> <span class="auth-label">Sign in</span> </a> @@ -91,9 +97,9 @@ <div class="page-head"> <div class="page-head-row"> <h1>Knowledge base</h1> - <button type="button" class="sync-btn" id="sync-btn" aria-label="Sync sources"> + <button type="button" class="sync-btn" id="sync-btn" aria-label="Sync sources" hidden> <svg class="sync-icon" aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/></svg> - <span class="sync-label">Sync sources</span> + <span class="sync-label" id="sync-label">Sync sources</span> </button> </div> <p class="page-sub"> diff --git a/frontend/tuning.html b/frontend/tuning.html index 97ab85d..ee976c4 100644 --- a/frontend/tuning.html +++ b/frontend/tuning.html @@ -31,18 +31,24 @@ permission 2026-08-23) — hidden by default, header.js reveals it once whoami says admin. The soft-gated page itself is unchanged. --> - <a href="/sources.html" class="nav-link" id="nav-sources" hidden>Sources</a> + <a href="/sources.html" class="nav-link" id="nav-sources" hidden>RAG</a> <!-- Phase 35 (owner permission 2026-08-26): the Git sources link is admin-only — hidden by default, header.js reveals it once whoami says admin, exactly like the Sources link above. --> - <a href="/git-sources.html" class="nav-link" id="nav-git-sources" hidden>Git sources</a> + <a href="/git-sources.html" class="nav-link" id="nav-git-sources" hidden>Sources</a> <!-- Phase 29 (now every page — phase 34, owner confirmation 2026-08-26): the Global Tuning link is admin-only (owner permission 2026-08-25) — hidden by default, header.js reveals it once whoami says admin, exactly like the Sources link above. --> <a href="/tuning.html" class="nav-link is-active" aria-current="page" id="nav-tuning" hidden>Tuning</a> + <!-- Phase 46 (mobile dropdown copy: sign-in — desktop bar copy is + outside the nav; see styles.css .sign-in-mobile rules). --> + <a href="/login.html?next=/" class="auth-link sign-in-link sign-in-mobile" id="sign-in-link-mobile" hidden> + <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M10 4h8a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-8"/><path d="M4 12h11"/><path d="m12 9 3 3-3 3"/></svg> + <span class="auth-label">Sign in</span> + </a> <!-- Phase 46 (mobile dropdown copy — desktop bar copy is outside the nav; see styles.css .sign-out-mobile rules). --> <button type="button" class="auth-link sign-out-btn sign-out-mobile" id="sign-out-btn-mobile" aria-label="Sign out" hidden> @@ -60,7 +66,7 @@ out is visible; /api/whoami decides at load (the shared header module). Icon-only below 640px (aria-labels keep the accessible names). --> - <a href="/login.html?next=/tuning.html" class="auth-link" id="sign-in-link" hidden> + <a href="/login.html?next=/tuning.html" class="auth-link sign-in-link" id="sign-in-link" hidden> <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M10 4h8a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-8"/><path d="M4 12h11"/><path d="m12 9 3 3-3 3"/></svg> <span class="auth-label">Sign in</span> </a> diff --git a/tests/e2e/test_git_sources_admin.py b/tests/e2e/test_git_sources_admin.py index dc13f7d..be0453a 100644 --- a/tests/e2e/test_git_sources_admin.py +++ b/tests/e2e/test_git_sources_admin.py @@ -82,7 +82,7 @@ REPO = Path(__file__).resolve().parents[2] APP_URL = f"http://127.0.0.1:{APP_PORT}" #: The five pages that ship the header (phase 34 contract) — the pages -#: task 05 gave the admin-only "Git sources" nav link. +#: task 05 gave the admin-only "Sources" nav link (then "Git sources"). CHAT_URL = "/" SOURCES_URL = "/sources.html" VIEWER_URL = "/document.html?source=docs&path=homelab%2Fkubernetes.md" @@ -307,7 +307,7 @@ def test_admin_nav_link_on_all_five_pages_and_click_navigates( expect(page).to_have_url(app_url + CHAT_URL, timeout=30_000) # The link is revealed (admin) on every one of the five pages, - # pointing at the git sources page, labeled "Git sources" — and it + # pointing at the git sources page, labeled "Sources" — and it # is NOT the current page on the four non-git-sources pages. for _name, path in FIVE_PAGES: if path != CHAT_URL: @@ -316,7 +316,7 @@ def test_admin_nav_link_on_all_five_pages_and_click_navigates( link = page.locator("#nav-git-sources") expect(link).to_be_visible(timeout=15_000) expect(link).to_have_attribute("href", GIT_SOURCES_URL) - expect(link).to_have_text("Git sources") + expect(link).to_have_text("Sources") if path != GIT_SOURCES_URL: expect(link).not_to_have_class(IS_ACTIVE) diff --git a/tests/e2e/test_mobile_hamburger_nav.py b/tests/e2e/test_mobile_hamburger_nav.py index 0ded8a8..699b850 100644 --- a/tests/e2e/test_mobile_hamburger_nav.py +++ b/tests/e2e/test_mobile_hamburger_nav.py @@ -32,7 +32,7 @@ Test → story mapping (Playwright Mapping Rule): 3. ``test_admin_menu_contents`` — admin at 375px: the menu shows all four links (the whoami reveal works inside the menu). 4. ``test_link_click_navigates_and_closes`` — admin at 375px: clicking - "Sources" navigates to /sources.html and the menu on the arrival + "RAG" navigates to /sources.html and the menu on the arrival page ships closed. 5. ``test_esc_and_outside_close`` — Esc closes AND returns focus to the toggle; an outside click does NOT close (accepted — see the test @@ -58,7 +58,7 @@ MOBILE: ViewportSize = {"width": 375, "height": 812} # the story's phone viewpo DESKTOP: ViewportSize = {"width": 1280, "height": 800} # the conftest page size NAV_LINKS = ("#app-nav a[href='/']", "#nav-sources", "#nav-git-sources", "#nav-tuning") -LINK_TEXTS = ("Chat", "Sources", "Git sources", "Tuning") +LINK_TEXTS = ("Chat", "RAG", "Sources", "Tuning") def _mobile_page(browser: Browser) -> Page: @@ -67,16 +67,25 @@ def _mobile_page(browser: Browser) -> Page: def _wait_settled_anonymous(page: Page) -> None: - """Wait until whoami has resolved for the anonymous visitor (Sign in - visible — the phase-16 settled state the header pins).""" - expect(page.locator("#sign-in-link")).to_be_visible(timeout=10_000) + """Wait until whoami has resolved for the anonymous visitor: the bar + Sign in copy (``#sign-in-link``) loses its ship-hidden attribute — + the phase-16 settled state, probed by attribute (not visibility): + at ≤640px the bar copy is CSS-hidden behind the ``#sign-in-link-mobile`` + dropdown copy (phase 46), so visibility is viewport-dependent.""" + page.wait_for_function( + "() => !document.querySelector('#sign-in-link').hasAttribute('hidden')", + timeout=10_000, + ) def _wait_settled_admin(page: Page) -> None: - """Wait until whoami has resolved for the admin (Sign out visible) - AND the whoami reveal has un-hidden the admin-only nav links (the - menu-contents assertions must run on a settled auth state).""" - expect(page.locator("#sign-out-btn")).to_be_visible(timeout=10_000) + """Wait until whoami has resolved for the admin: the whoami reveal + has un-hidden the admin-only nav links (the menu-contents assertions + must run on a settled auth state). The nav link is the + viewport-independent settled signal — the sign-out control is the + bar copy on desktop but the #sign-out-btn-mobile dropdown copy at + ≤640px (phase-46 UX revision), so it is not a cross-viewport + probe.""" page.wait_for_function( "() => !document.querySelector('#nav-sources').hasAttribute('hidden')", timeout=10_000, @@ -205,7 +214,7 @@ def test_admin_menu_contents( browser: Browser, app_url: str, db_ready: None ) -> None: """AC2 (admin): at 375px the opened menu shows ALL FOUR links — - Chat / Sources / Git sources / Tuning — i.e. the whoami reveal + Chat / RAG / Sources / Tuning — i.e. the whoami reveal works inside the menu exactly as it does inline (one <nav>, one set of links, the same hidden attributes header.js drives).""" page = _mobile_page(browser) diff --git a/tests/e2e/test_nav_consistency.py b/tests/e2e/test_nav_consistency.py index 6da8dfa..eba0881 100644 --- a/tests/e2e/test_nav_consistency.py +++ b/tests/e2e/test_nav_consistency.py @@ -19,19 +19,22 @@ sources, document viewer, global tuning, login): one shared markup block Per role, the VISIBLE inventory: * admin: brand + nav [Chat, #nav-sources, #nav-git-sources, #nav-tuning] - (four links, that order — the Git sources link joined in phase 35, - owner permission 2026-08-26) + #sync-btn + #new-chat-btn + - #sign-out-btn (with #sign-in-link hidden) — on all five pages, same - id+class inventory, same DOM order. The #steering-toggle was removed - from the navbar at owner request (2026-08-28); note management lives - on /tuning.html; + (four links, that order — the "Sources" link joined in phase 35 as + "Git sources", owner permission 2026-08-26) + #sign-out-btn (with + #sign-in-link + hidden) — on all five pages, same id+class inventory, same DOM order. + The #sync-btn (Sources page only) and the #new-chat-btn (chat page + only) left the shared bar at owner request (2026-08-28 — they are + page-specific now, so the per-page visible inventory differs for + exactly those two); the #steering-toggle was removed from the navbar + the same day; note management lives on /tuning.html; * anonymous: brand + nav [Chat] (#nav-sources / #nav-git-sources / - #nav-tuning hidden — locked A10 UI revision) + #new-chat-btn + - #sign-in-link (with #sync-btn hidden, #sign-out-btn hidden) on all - five pages — and the steering toggle (removed at owner request, - 2026-08-28) + panel are ABSENT from the DOM (the panel via the phase - 16 "absent, not hidden" treatment, carried into phase 34 task 01; - test_admin_auth pins it). + #nav-tuning hidden — locked A10 UI revision) + #sign-in-link (with + #sign-out-btn hidden; the Sources page's #sync-btn stays ship-hidden) + on all five pages — and the steering toggle (removed at owner + request, 2026-08-28) + panel are ABSENT from the DOM (the panel via + the phase 16 "absent, not hidden" treatment, carried into phase 34 + task 01; test_admin_auth pins it). Normalization for the inventory comparison: the current-page ``is-active`` nav marker and the sign-in ``?next=`` value legitimately differ per page, @@ -47,9 +50,10 @@ Steering off-chat: on /tuning.html (admin, zero notes) the navbar carries no steering toggle (removed at owner request, 2026-08-28) — the header #steering-panel section still ships hidden and the Tuning page's own note list shows the empty state — no chat needed. Sync is -present, not triggered: #sync-btn is visible on /tuning.html but is -never clicked here (a real sync clones real repos — the full state -machine is test_sync_button.py's job). +present, not triggered: #sync-btn is visible on /sources.html (its +home since the owner rework 2026-08-28) but is never clicked here (a +real sync clones real repos — the full state machine is +test_sync_button.py's job). Determinism note: every assertion is settled-state — each page visit first waits for the whoami toggle to land (exactly one of Sign in / @@ -65,7 +69,7 @@ Test → story mapping (Playwright Mapping Rule): 3. ``test_viewer_row1_height_matches_chat_and_titlebar_present`` 4. ``test_viewer_back_link_honors_back_param`` 5. ``test_steering_surface_off_chat_on_tuning_page`` -6. ``test_sync_button_present_on_tuning_page_without_triggering`` +6. ``test_sync_button_present_on_sources_page_without_triggering`` """ from __future__ import annotations @@ -177,7 +181,11 @@ def _header_inventory(page: Page) -> list[str]: ``page`` is showing (normalized — see _INVENTORY_JS).""" inv = page.evaluate(_INVENTORY_JS) assert inv is not None, "no `header .header-inner` on this page" - assert len(inv) >= 8, f"header control inventory unexpectedly short: {inv}" + # Shared bar: brand + the four nav links + Sign in + Sign out = 7 + # (the #sync-btn / #new-chat-btn selectors keep matching nothing — + # they left the bar with the owner rework 2026-08-28; the steering + # toggle was removed the same day). + assert len(inv) >= 7, f"header control inventory unexpectedly short: {inv}" return inv @@ -220,8 +228,9 @@ def _visit(page: Page, app_url: str, name: str, url: str, admin: bool) -> list[s expect(page.locator(".app-nav a[href='/']")).to_be_visible() # Chat if admin: expect(page.locator("#nav-sources")).to_be_visible() - # Phase 35: the fourth admin-only nav link (Git sources) is - # revealed on every page, between Sources and Tuning. + # Phase 35: the fourth admin-only nav link (now "Sources", + # shipped as "Git sources") is revealed on every page, between + # RAG and Tuning. expect(page.locator("#nav-git-sources")).to_be_visible() expect(page.locator("#nav-tuning")).to_be_visible() # The steering toggle was removed from the navbar at owner @@ -229,7 +238,15 @@ def _visit(page: Page, app_url: str, name: str, url: str, admin: bool) -> list[s assert page.locator("#steering-toggle").count() == 0, ( f"{name}: the steering toggle was removed from the navbar" ) - expect(page.locator("#sync-btn")).to_be_visible() + # The Sync button is a page-specific control (Sources page only + # — owner rework 2026-08-28): visible on sources, absent from + # the shared bar everywhere else. + if name == "sources": + expect(page.locator("#sync-btn")).to_be_visible() + else: + assert page.locator("#sync-btn").count() == 0, ( + f"{name}: #sync-btn left the shared bar (Sources page only)" + ) expect(page.locator("#sign-out-btn")).to_be_visible() expect(page.locator("#sign-in-link")).to_be_hidden() else: @@ -252,7 +269,14 @@ def _visit(page: Page, app_url: str, name: str, url: str, admin: bool) -> list[s assert page.locator("#steering-panel").count() == 0, ( f"{name}: the steering panel must be absent for anonymous" ) - expect(page.locator("#new-chat-btn")).to_be_visible() + # The New chat button is chat-page only (moved from the shared bar + # to index.html's .chat-shell at owner request, 2026-08-28). + if name == "chat": + expect(page.locator("#new-chat-btn")).to_be_visible() + else: + assert page.locator("#new-chat-btn").count() == 0, ( + f"{name}: #new-chat-btn left the shared bar (chat page only)" + ) _assert_landmarks(page, name) return _header_inventory(page) @@ -302,10 +326,12 @@ def _admin_login_page_inventory(page: Page, app_url: str) -> list[str]: assert page.locator("#steering-toggle").count() == 0, ( "login: the steering toggle was removed from the navbar" ) - expect(page.locator("#sync-btn")).to_be_visible() + # Page-specific controls are NOT on the auth page (owner rework + # 2026-08-28: sync → Sources page, new chat → chat page). + assert page.locator("#sync-btn").count() == 0 + assert page.locator("#new-chat-btn").count() == 0 expect(page.locator("#sign-out-btn")).to_be_visible() expect(page.locator("#sign-in-link")).to_be_hidden() - expect(page.locator("#new-chat-btn")).to_be_visible() _assert_landmarks(page, "login") return _header_inventory(page) finally: @@ -482,14 +508,16 @@ def test_steering_surface_off_chat_on_tuning_page( # --------------------------------------------------------------------------- -def test_sync_button_present_on_tuning_page_without_triggering( +def test_sync_button_present_on_sources_page_without_triggering( page: Page, app_url: str, mock_llm: int, db_ready: None ) -> None: page.set_viewport_size({"width": 1280, "height": 800}) _seed_db(mock_llm) - login(page, app_url, next=TUNING_URL) - expect(page).to_have_url(app_url + TUNING_URL, timeout=30_000) + # The button's home is the Sources page (owner rework 2026-08-28 — + # it left the shared navbar). + login(page, app_url, next=SOURCES_URL) + expect(page).to_have_url(app_url + SOURCES_URL, timeout=30_000) expect(page.locator("#sign-out-btn")).to_be_visible(timeout=15_000) btn = page.locator("#sync-btn") diff --git a/tests/e2e/test_nav_rename_sources.py b/tests/e2e/test_nav_rename_sources.py new file mode 100644 index 0000000..2e80067 --- /dev/null +++ b/tests/e2e/test_nav_rename_sources.py @@ -0,0 +1,400 @@ +"""Phase 48 story E2E (Playwright): the nav rename — "Sources" becomes +"RAG", "Git sources" becomes "Sources". + +Story: ``.agent/user_stories/nav-sources-rag-rename.md`` +Run in isolation (DB must be up: ``podman compose up -d db``): + + uv run pytest tests/e2e/test_nav_rename_sources.py -v --no-cov + +Owner request (2026-08-28): the two admin-only nav items read like the +same thing, so they are relabeled — the document-catalog link +(``#nav-sources`` → /sources.html) becomes **"RAG"** and the +source-manager link (``#nav-git-sources`` → /git-sources.html) becomes +**"Sources"**. Everything else is UNCHANGED (the phase's locked +decision, label-only): element ids, hrefs, the physical nav order, the +phase-16/19/35 ship-hidden/reveal contract, ``header.js`` behavior, and +every other label on the pages — the document viewer's "Sources" back +button (a different control, phase 13) and the "Sync sources" button +(phase 32) in particular. + +The six pages under test (all carry the one shared header, phase +19/34): chat (/), the RAG catalog (/sources.html), the Sources manager +(/git-sources.html), Tuning (/tuning.html), the login page +(/login.html), and the document viewer (/document.html — seeded with +one fixture document row first, the test_nav_consistency.py viewer +pattern; #doc-title must settle before any bar assertion). + +Contract under test (desktop viewport 1280×800, settled whoami state — +every assertion waits for the initSharedHeader pass to land first): + +* admin: on EACH of the six pages ``#nav-sources`` is visible with the + exact text "RAG" and href /sources.html, ``#nav-git-sources`` is + visible with the exact text "Sources" and href /git-sources.html, + and the nav DOM order reads Chat, RAG, Sources, Tuning; the + current page's link is the ONLY one carrying is-active + + aria-current="page" (the login and viewer pages mark none — + neither is a nav page). +* from the chat page: clicking "RAG" (``#nav-sources``) lands on + /sources.html with that link active; clicking "Sources" + (``#nav-git-sources``) lands on /git-sources.html with that link + active. +* anonymous: on / and /login.html both links are PRESENT in the DOM + (the ship-hidden contract — header.js toggles the hidden attribute, + the markup is never removed) but hidden, and #sign-in-link is + visible. +* the rename did not leak: on /sources.html the Sync button still + reads "Sync sources" (``#sync-label``), and on the settled viewer + page the back button's span still reads "Sources" (href + /sources.html). + +Determinism note: the seed truncates documents/chunks/query_log/ +steering_notes and re-imports the fixture docs (mock embeddings) so the +viewer URL resolves to "Kubernetes Homelab Cluster" on every run. No +chat turn is submitted and #sync-btn is never clicked. + +Test → story mapping (Playwright Mapping Rule): +1. ``test_admin_labels_on_all_six_pages`` +2. ``test_click_navigates_with_marker`` +3. ``test_anonymous_sees_neither`` +4. ``test_untouched_controls_stay`` +""" +from __future__ import annotations + +import asyncio +import re +from pathlib import Path +from threading import Thread +from typing import Any + +from playwright.sync_api import Page, expect +from sqlalchemy import text + +from app.config import Settings +from app.db import SessionLocal +from app.rag.importer import ImportSummary, import_sources +from app.rag.llm import LLMClient +from e2e.auth_helpers import login + +REPO = Path(__file__).resolve().parents[2] +FIXTURES = REPO / "tests" / "fixtures" / "docs" + +CHAT_URL = "/" +SOURCES_URL = "/sources.html" +GIT_SOURCES_URL = "/git-sources.html" +TUNING_URL = "/tuning.html" +LOGIN_URL = "/login.html" +#: A seeded fixture doc (source=docs), URL-encoded — the same document +#: every viewer suite uses (title "Kubernetes Homelab Cluster"). +VIEWER_URL = "/document.html?source=docs&path=homelab%2Fkubernetes.md" +DOC_TITLE = "Kubernetes Homelab Cluster" + +#: The six pages of the app, in the story's order. +SIX_PAGES = ( + ("chat", CHAT_URL), + ("sources", SOURCES_URL), + ("git-sources", GIT_SOURCES_URL), + ("tuning", TUNING_URL), + ("login", LOGIN_URL), + ("viewer", VIEWER_URL), +) + +#: The locator of the link that carries the current-page marker on each +#: page (None — login and viewer are not nav pages — marks none). +CURRENT_LINK: dict[str, str | None] = { + "chat": ".app-nav a[href='/']", + "sources": "#nav-sources", + "git-sources": "#nav-git-sources", + "tuning": "#nav-tuning", + "login": None, + "viewer": None, +} + +#: The four primary nav links, in their physical DOM order — the labels +#: after the phase-48 swap (ids/hrefs unchanged). +NAV_LABELS = ("Chat", "RAG", "Sources", "Tuning") + +#: The login.js script — route pattern for the redirect suppression. +LOGIN_JS_ROUTE = re.compile(r"/assets/login\.js(\?.*)?$") + +#: is-active as a word-boundary regex (to_have_class matches against the +#: whole class string — the test_git_sources_admin.py convention). +IS_ACTIVE = re.compile(r"\bis-active\b") + + +async def _import_fixtures(mock_port: int) -> ImportSummary: + kwargs: dict[str, Any] = {"_env_file": None, "llm_base_url": f"http://127.0.0.1:{mock_port}/v1"} + settings = Settings(**kwargs) # pyright: ignore[reportCallIssue] + return await import_sources([FIXTURES], LLMClient(settings)) + + +def _run_in_thread(coro: Any) -> Any: + """Run a coroutine on a worker thread (Playwright owns the test loop).""" + box: dict[str, Any] = {} + + def runner() -> None: + try: + box["value"] = asyncio.run(coro) + except BaseException as e: # noqa: BLE001 — re-raised on the test thread + box["error"] = e + + t = Thread(target=runner) + t.start() + t.join() + if "error" in box: + raise box["error"] + return box["value"] + + +def _seed_db(mock_port: int) -> None: + """Fresh KB + the fixture docs so the viewer URL resolves (the + test_nav_consistency.py seeding pattern).""" + with SessionLocal() as db: + db.execute(text("TRUNCATE chunks, documents, query_log, steering_notes")) + db.commit() + _run_in_thread(_import_fixtures(mock_port)) + + +def _wait_settled_admin(page: Page) -> None: + """Wait for initSharedHeader's whoami toggle to land for a signed-in + admin: the whoami reveal has un-hidden the admin-only nav links. The + nav link is the viewport-independent settled signal (the auth pair is + the bar copy on desktop but the dropdown copy at ≤640px, phase 46) — + and the sign-in/out state settles in the SAME initSharedHeader pass. + """ + page.wait_for_function( + "() => !document.querySelector('#nav-sources').hasAttribute('hidden')", + timeout=15_000, + ) + + +def _wait_settled_anonymous(page: Page) -> None: + """Wait for the whoami toggle to land for an anonymous visitor: the + bar Sign in copy (``#sign-in-link``) loses its ship-hidden attribute + (probed by attribute — at ≤640px the bar copy is CSS-hidden behind + the dropdown copy, phase 46).""" + page.wait_for_function( + "() => !document.querySelector('#sign-in-link').hasAttribute('hidden')", + timeout=15_000, + ) + + +def _assert_renamed_labels(page: Page, name: str) -> None: + """AC1 on one page: the swapped labels, the unchanged hrefs, and the + nav DOM order (Chat, RAG, Sources, Tuning) on a settled admin bar.""" + rag = page.locator("#nav-sources") + expect(rag).to_be_visible(timeout=15_000) + expect(rag).to_have_text("RAG") + expect(rag).to_have_attribute("href", "/sources.html") + + git = page.locator("#nav-git-sources") + expect(git).to_be_visible(timeout=15_000) + expect(git).to_have_text("Sources") + expect(git).to_have_attribute("href", "/git-sources.html") + + # The four primary nav links (class nav-link) in physical DOM order. + nav_texts = page.eval_on_selector_all( + ".app-nav a.nav-link", "els => els.map(e => e.textContent.trim())" + ) + assert nav_texts == list(NAV_LABELS), ( + f"{name}: nav link order/labels are {nav_texts}, expected {list(NAV_LABELS)}" + ) + # …and the full anchor sequence of the nav (it also carries the + # phase-46 mobile sign-in copy) opens with the same four, in order. + all_texts = page.eval_on_selector_all( + ".app-nav a", "els => els.map(e => e.textContent.trim())" + ) + assert all_texts[:4] == list(NAV_LABELS), ( + f"{name}: .app-nav anchor sequence {all_texts} does not open with " + f"Chat, RAG, Sources, Tuning" + ) + + +def _assert_current_marker(page: Page, name: str) -> None: + """AC1 on one page: the current page's link carries is-active + + aria-current="page" — and ONLY it does (login/viewer mark none).""" + current = CURRENT_LINK[name] + if current is None: + assert page.locator(".app-nav a.is-active").count() == 0, ( + f"{name}: no nav page is current — no link may carry is-active" + ) + assert page.locator('.app-nav a[aria-current="page"]').count() == 0, ( + f"{name}: no nav page is current — no link may carry aria-current" + ) + return + expect(page.locator(current)).to_have_class(IS_ACTIVE) + expect(page.locator(current)).to_have_attribute("aria-current", "page") + assert page.locator(".app-nav a.is-active").count() == 1, ( + f"{name}: exactly one nav link may carry is-active" + ) + assert page.locator('.app-nav a[aria-current="page"]').count() == 1, ( + f"{name}: exactly one nav link may carry aria-current" + ) + + +def _visit_admin_page(page: Page, app_url: str, name: str, url: str) -> None: + """Goto a page as the signed-in admin, wait for the settled header + (and the document title on the viewer), assert the rename contract.""" + page.goto(app_url + url) + _wait_settled_admin(page) + if name == "viewer": + # The document itself has settled (rendered, not Loading…/ + # not-found) before any bar assertion — the test_nav_consistency + # viewer-pass pattern. + expect(page.locator("#doc-title")).to_have_text(DOC_TITLE, timeout=15_000) + _assert_renamed_labels(page, name) + _assert_current_marker(page, name) + + +def _visit_login_as_admin(page: Page, app_url: str) -> None: + """The login page redirects a signed-in admin away (login.js — + phase 16), so this ONE visit serves login.js with the redirect lines + suppressed (a test-local route, the test_nav_consistency.py pattern; + the page's header — settled by the same initSharedHeader pass — is + what gets measured, and the page stays put). + + The browser cache is cleared first: phase 33 caches ``/assets/*`` + ``immutable`` for a year, and the earlier form login already fetched + the (unmodified) login.js — a cache hit would bypass the route. + """ + login_js = (REPO / "frontend" / "assets" / "login.js").read_text(encoding="utf-8") + assert "window.location.replace(safeNext())" in login_js + suppressed = login_js.replace( + "window.location.replace(safeNext())", + "window.__e2e_redirectSuppressed = true; // test: observe the header", + ) + page.route( + LOGIN_JS_ROUTE, + lambda route: route.fulfill( + status=200, content_type="text/javascript", body=suppressed + ), + ) + try: + cdp = page.context.new_cdp_session(page) + try: + cdp.send("Network.clearBrowserCache") + finally: + cdp.detach() + page.goto(app_url + LOGIN_URL) + expect(page).to_have_url(app_url + LOGIN_URL, timeout=15_000) + _visit_admin_page(page, app_url, "login", LOGIN_URL) + finally: + page.unroute(LOGIN_JS_ROUTE) + + +# --------------------------------------------------------------------------- +# 1. Admin: the swapped labels, unchanged hrefs/order/markers, on all +# six pages +# --------------------------------------------------------------------------- + + +def test_admin_labels_on_all_six_pages( + page: Page, app_url: str, mock_llm: int, db_ready: None +) -> None: + page.set_viewport_size({"width": 1280, "height": 800}) + _seed_db(mock_llm) + + login(page, app_url, next=CHAT_URL) + expect(page).to_have_url(app_url + CHAT_URL, timeout=30_000) + + for name, url in SIX_PAGES: + if name == "login": + _visit_login_as_admin(page, app_url) + else: + _visit_admin_page(page, app_url, name, url) + + +# --------------------------------------------------------------------------- +# 2. The renamed links navigate: "RAG" → /sources.html (active), +# "Sources" → /git-sources.html (active) +# --------------------------------------------------------------------------- + + +def test_click_navigates_with_marker( + page: Page, app_url: str, mock_llm: int, db_ready: None +) -> None: + page.set_viewport_size({"width": 1280, "height": 800}) + _seed_db(mock_llm) + + login(page, app_url, next=CHAT_URL) + expect(page).to_have_url(app_url + CHAT_URL, timeout=30_000) + _wait_settled_admin(page) + + # "RAG" (the renamed catalog label) → the RAG catalog page, where + # #nav-sources is the active link. + expect(page.locator("#nav-sources")).to_have_text("RAG") + page.click("#nav-sources") + expect(page).to_have_url(app_url + SOURCES_URL, timeout=30_000) + _wait_settled_admin(page) + _assert_current_marker(page, "sources") + + # "Sources" (the renamed manager label) → the Sources manager page, + # where #nav-git-sources is the active link. + page.goto(app_url + CHAT_URL) + _wait_settled_admin(page) + expect(page.locator("#nav-git-sources")).to_have_text("Sources") + page.click("#nav-git-sources") + expect(page).to_have_url(app_url + GIT_SOURCES_URL, timeout=30_000) + _wait_settled_admin(page) + _assert_current_marker(page, "git-sources") + + +# --------------------------------------------------------------------------- +# 3. Anonymous: both links present in the DOM (ship-hidden contract) but +# hidden — #sign-in-link visible +# --------------------------------------------------------------------------- + + +def test_anonymous_sees_neither( + page: Page, app_url: str, mock_llm: int, db_ready: None +) -> None: + page.set_viewport_size({"width": 1280, "height": 800}) + _seed_db(mock_llm) + + # No login: a fresh context is anonymous by construction. + for name, url in (("chat", CHAT_URL), ("login", LOGIN_URL)): + page.goto(app_url + url) + _wait_settled_anonymous(page) + rag = page.locator("#nav-sources") + git = page.locator("#nav-git-sources") + # Present in the DOM (the markup ships, header.js toggles the + # hidden attribute)… + assert rag.count() == 1, f"{name}: #nav-sources must be in the DOM" + assert git.count() == 1, f"{name}: #nav-git-sources must be in the DOM" + # …and hidden for anonymous (the ship-hidden contract, unchanged + # by the rename). + expect(rag).to_be_hidden() + expect(git).to_be_hidden() + # The reduced bar's settled sign-in control is visible. + expect(page.locator("#sign-in-link")).to_be_visible() + + +# --------------------------------------------------------------------------- +# 4. The rename did not leak: the Sync button label and the viewer back +# button label (different controls) are untouched +# --------------------------------------------------------------------------- + + +def test_untouched_controls_stay( + page: Page, app_url: str, mock_llm: int, db_ready: None +) -> None: + page.set_viewport_size({"width": 1280, "height": 800}) + _seed_db(mock_llm) + + # On /sources.html (admin — the button ships hidden and the page + # boot reveals it on the same cached whoami) the Sync button still + # reads "Sync sources". + login(page, app_url, next=SOURCES_URL) + expect(page).to_have_url(app_url + SOURCES_URL, timeout=30_000) + _wait_settled_admin(page) + expect(page.locator("#sync-btn")).to_be_visible(timeout=15_000) + expect(page.locator("#sync-label")).to_have_text("Sync sources") + # Never clicked — a real sync is test_sync_button.py's job. + + # On the settled viewer page the back button's span still reads + # "Sources" (the viewer back link is a different control — phase 13; + # the rename only touched the nav items). + page.goto(app_url + VIEWER_URL) + expect(page.locator("#doc-title")).to_have_text(DOC_TITLE, timeout=15_000) + back = page.locator("#doc-back") + expect(back).to_have_attribute("href", "/sources.html") + expect(back.locator("span")).to_have_text("Sources") diff --git a/tests/e2e/test_shared_header.py b/tests/e2e/test_shared_header.py index fc96574..5954918 100644 --- a/tests/e2e/test_shared_header.py +++ b/tests/e2e/test_shared_header.py @@ -8,13 +8,15 @@ Run in isolation (DB must be up: ``podman compose up -d db``): Contract under test (owner report 2026-08-23, phase 19) — ONE bar per page, the same controls everywhere: -* chat / sources / viewer: brand + nav [Chat, Sources — admin only] + - New Chat + Sign in / Sign out; +* chat / sources / viewer: brand + nav [Chat, RAG — admin only, + the + "Sources" link (phase 35, shipped as "Git sources")] + Sign in / + Sign out. The New Chat button is chat-page only — it left the shared + bar at owner request (2026-08-28, moved to index.html's .chat-shell); * document viewer: the standard bar (row 1) + back + title + meta in a second titlebar row (phase 34, owner confirmation 2026-08-26 — the viewer's old "no nav" single-row bar is superseded; it now carries the SAME nav contract as every other page); -* the "Sources" nav link (``#nav-sources``) is HIDDEN for anonymous +* the "RAG" nav link (``#nav-sources``) is HIDDEN for anonymous users on every page and shown for admin (phase-16 UX revision with owner permission; the soft-gate page and the A10 API split are untouched) — now on the viewer as well (phase 34); @@ -33,7 +35,7 @@ Test → story mapping (Playwright Mapping Rule): 1. ``test_anonymous_bar_on_all_pages`` 2. ``test_admin_bar_on_all_pages`` 3. ``test_sources_nav_hidden_for_anonymous_everywhere`` -4. ``test_new_chat_from_sources_clears_and_navigates`` +4. ``test_new_chat_button_is_chat_page_only`` 5. ``test_sign_out_from_viewer_returns_to_anonymous`` 6. ``test_mobile_bar_fits_and_heights_held`` """ @@ -136,17 +138,33 @@ def assert_shared_bar(page: Page, admin: bool, page_kind: str, mobile: bool = Fa pinned by ``test_mobile_hamburger_nav.py`` (phase 46, task 03); this helper pins the bar-level contract only. """ - # Settled auth state: exactly one of Sign in / Sign out is visible - # (phase-16 semantics, now owned by the shared module). + # Settled auth state: exactly one of Sign in / Sign out is + # revealed (phase-16 semantics, now owned by the shared module). + # Both probes are viewport-independent attribute checks — at ≤640px + # the bar auth copies are CSS-hidden behind the #sign-in-link-mobile + # / #sign-out-btn-mobile dropdown copies (phase-46 UX revision), so + # visibility is not a cross-viewport probe. if admin: - expect(page.locator("#sign-out-btn")).to_be_visible(timeout=15_000) + page.wait_for_function( + "() => !document.querySelector('#nav-sources').hasAttribute('hidden')", + timeout=15_000, + ) expect(page.locator("#sign-in-link")).to_be_hidden() else: - expect(page.locator("#sign-in-link")).to_be_visible(timeout=15_000) + page.wait_for_function( + "() => !document.querySelector('#sign-in-link').hasAttribute('hidden')", + timeout=15_000, + ) expect(page.locator("#sign-out-btn")).to_be_hidden() - # New Chat is on the bar on every page kind (the owner's ask). - expect(page.locator("#new-chat-btn")).to_be_visible() + # The New chat button is chat-page only (moved from the shared bar + # to index.html's .chat-shell at owner request, 2026-08-28). + if page_kind == "chat": + expect(page.locator("#new-chat-btn")).to_be_visible() + else: + assert page.locator("#new-chat-btn").count() == 0, ( + f"{page_kind}: the New chat button is chat-page only" + ) # Phase 34: EVERY page kind — viewer included — carries the SAME # nav contract: the Chat link always visible; the admin-only @@ -246,7 +264,7 @@ def test_admin_bar_on_all_pages( # --------------------------------------------------------------------------- -# 3. The Sources nav link: hidden for anonymous everywhere, revealed +# 3. The RAG nav link: hidden for anonymous everywhere, revealed # after a real login (a toggle, not just initial state) # --------------------------------------------------------------------------- @@ -282,51 +300,26 @@ def test_sources_nav_hidden_for_anonymous_everywhere( # --------------------------------------------------------------------------- -# 4. New Chat from a non-chat page: clear the conversation, land on the -# chat empty state +# 4. The New chat button is chat-page only (owner rework 2026-08-28 — +# it left the shared bar with the 'go to the chat, fresh' behavior) # --------------------------------------------------------------------------- -def test_new_chat_from_sources_clears_and_navigates( - page: Page, app_url: str, db_ready: None +def test_new_chat_button_is_chat_page_only( + page: Page, app_url: str, mock_llm: int, db_ready: None ) -> None: page.set_viewport_size({"width": 1280, "height": 800}) + _seed_db(mock_llm) - # Seed the phase-14 conversation before any page script runs. The - # init script runs on EVERY navigation, so it is scoped to the - # sources page — the post-click navigation to "/" must start clean. - page.add_init_script( - """(() => { - if (location.pathname !== "/sources.html") return; - try { - localStorage.setItem("bor.chat.v1", JSON.stringify({ - v: 1, - messages: [ - { who: "user", text: "hello brain" }, - { who: "brain", text: "hey there" } - ] - })); - } catch {} - })();""" - ) + # No non-chat page carries the button anymore… + for path in (SOURCES_URL, VIEWER_URL, "/tuning.html", "/login.html", "/git-sources.html"): + page.goto(app_url + path) + expect(page.locator("#new-chat-btn")).to_have_count(0) - page.goto(app_url + SOURCES_URL) - # The seeded conversation is in storage… - assert ( - page.evaluate("() => localStorage.getItem('bor.chat.v1')") is not None - ), "init script must have seeded the phase-14 conversation key" - - # New Chat from the sources page: a new chat means going to the - # chat — fresh. - page.click("#new-chat-btn") - expect(page).to_have_url(app_url + "/", timeout=30_000) - - # …and the chat lands on its empty state with the key removed. - expect(page.locator("#empty-state")).to_be_visible() - expect(page.locator(".msg")).to_have_count(0) - assert page.evaluate("() => localStorage.getItem('bor.chat.v1')") is None, ( - "New Chat from a non-chat page must clear the localStorage key" - ) + # …and the chat page has exactly one (visible, inside .chat-shell). + page.goto(app_url + "/") + expect(page.locator("#new-chat-btn")).to_have_count(1) + expect(page.locator("#new-chat-btn")).to_be_visible() # ---------------------------------------------------------------------------