feat(ui): rename nav items — "Sources" becomes "RAG", "Git sources" becomes "Sources"

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 <a> 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.
This commit is contained in:
2026-08-28 12:33:18 -04:00
parent 03bead092c
commit 872a07cee7
17 changed files with 785 additions and 118 deletions
+42 -49
View File
@@ -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()
# ---------------------------------------------------------------------------