phase: 105_hidden_folders_toggle
Build and Push Containers / build-and-push-app (push) Successful in 1m44s
Build and Push Containers / build-and-push-db (push) Successful in 13s

All completion criteria verified. Everything is green.

**Phase 105 final verification pass — all criteria verified**

- Verified the full implementation in the working tree: `git_sources.include_hidden` column + alembic `0019` (dev DB at head, column present), `iter_importable_files`/`import_sources` flag support with `str(root)`-keyed map used by both walk and progress pre-walk, `GitSourcePatchIn` rename with optional fields, sync/CLI pipeline wiring (OR-collision), and the per-row "Hidden" checkbox + tag + error line on the Sources page
- Unit + integration: `uv run pytest` → exit 0 (2148 tests collected, all pass; this sandbox occasionally swallows pytest's final status line — exit codes verified)
- Coverage: `uv run pytest --cov=app --cov-report=term-missing` → **TOTAL 99%** (3879 stmts, 15 miss) — >90% gate ✓
- Dedicated E2E: `uv run pytest tests/e2e/test_hidden_folders_toggle.py -v --no-cov` → **6 passed in 23.20s** (DB up, in isolation)
- Regression E2E in isolation: `test_source_ignore_paths` 6 passed, `test_git_sources_admin` 6 passed, `test_local_directory_sources` 3 passed, `test_sync_button` 3 passed, `test_smoke` 3 passed
- Lint/types: `uv run ruff check .` + `uv run pyright` → clean (0 errors/warnings)

**Completion criteria:** (1) checkbox persists via PATCH 200 → "hidden on" tag + GET round-trips `include_hidden: true`; failure path reverts box + `role="alert"` canned message ✓; (2) flag OFF byte-identical (only `visible.md` indexed), ON indexes `.hidden/note.md` into the KB catalog, `EXCLUDED_DIRS` excluded both states ✓; (3) A2: flag OFF → `detail.pruned==1`, doc gone from catalog ✓; (4) PATCH bool-only/list-only/both/neither no-op, phase-89 fixed 422s unchanged, 404, anonymous 403 (incl. bool-only body) ✓; (5) env-fallback rows render no checkbox, WCAG-clean (aria-label, keyboard focus, visible label, text tag) ✓; (6) full gate green ✓; (7) commit left to the harness per instructions (no `git add`/`commit` run; phase files untouched).

**Deviations:** none — no defects found; no code changes were needed on this pass.

**Next pending phase:** `.agents/phases/todo/98_sync_summary_visibility`.
This commit is contained in:
2026-09-12 21:26:30 -04:00
parent ecc921098a
commit d731169b8b
49 changed files with 3553 additions and 93 deletions
+436
View File
@@ -0,0 +1,436 @@
"""Unit: the per-row "Hidden" toggle on the Sources page (phase 105,
task 05).
``TODO.md`` L3: "There should be a toggle per input (next to the
ignores button) to allow indexing hidden .folders." Every STORED
``git_sources`` row (git or local) gets a native labeled **Hidden**
checkbox in the actions cell, LEFT of the "Ignore paths" button (DOM
order Hidden · Ignore paths · Remove — A5); when on, the source cell
shows a "hidden on" text tag (text + background, never color alone —
WCAG 1.4.1); the flip PATCHes ``{"include_hidden": …}`` with the
§7.4 never-stale lifecycle (box disables at once; 200 → reload +
confirm LAST; failure → the box reverts to the SERVER state and the
detail lands in the page-level ``#git-sources-hidden-error``
``role="alert"`` line — the checkbox is a table-cell control with no
dialog of its own). Env-fallback rows (``id`` null) get no checkbox
(A3 — nothing is stored to flag).
The browser behavior itself is E2E-gated by the phase-105 story suite
(``tests/e2e/test_hidden_folders_toggle.py``, task 06); like the
phase-89 ``test_source_ignore_paths.py`` house pattern, this module
pins the source-level contract a silent regression would break:
* the JS row wiring — ``makeRow`` builds the checkbox ONLY in the
``s.id`` branch (class ``git-source-hidden-box``, ``type=checkbox``,
``checked`` from ``s.include_hidden === true`` — server state only,
never a prior local flip), the aria-label is the ONLY place the
value appears (setAttribute — never innerHTML), the visible
"Hidden" text label, and the append ORDER (hidden label →
ignore button → Remove); the "hidden on" tag is TEXT in the source
cell, iff ``s.id && s.include_hidden === true``;
* the JS lifecycle — ``toggleHidden``: the box disables BEFORE the
``PATCH`` (one flip at a time), the body is
``JSON.stringify({ include_hidden: wanted })`` ONLY (the row's
ignore list is untouched — task 03's optional field), 200 →
clear the error → ``await loadSources()`` → announce (the
confirmation is the LAST announcement); non-2xx AND network
failure → the box REVERTS to ``s.include_hidden === true`` +
re-enables and the detail lands in the page-level alert line;
a healed ``loadSources`` clears the line (the phase-89 "happy
path heals the error state" precedent);
* the cross-file wire contract (SINGLE SOURCE OF TRUTH for the
field name) — the JS PATCH body key and the render field both
match their ``app/schemas.py`` counterparts
(``GitSourcePatchIn.include_hidden`` / ``GitSourceRow``) — a
rename on either side breaks the test;
* the static shell markup — ``#git-sources-hidden-error`` is a
``role="alert"`` line, hidden by default, reusing the
``.git-source-error`` class, INSIDE ``#view-git-sources`` and
AFTER ``#git-sources-table-wrap`` in source order;
* styles.css — the ``.git-source-hidden`` family on the house
palette: ~44px hit height, the checkbox's ``accent-color:
var(--brand)`` (checked-state contrast verified + recorded in the
comment), the ``:disabled`` wait state, and the "hidden on" tag
(text + a distinct background, never color alone).
"""
from __future__ import annotations
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
FRONTEND = ROOT / "frontend"
# Phase 76 (task 02): the Sources view lives in the ONE-document shell.
SHELL_HTML = FRONTEND / "index.html"
JS = FRONTEND / "assets" / "git-sources.js"
CSS = FRONTEND / "assets" / "styles.css"
SCHEMAS = ROOT / "app" / "schemas.py"
#: The checkbox + tag classes (pinned verbatim).
HIDDEN_BOX_CLASS = "git-source-hidden-box"
HIDDEN_LABEL_CLASS = "git-source-hidden"
HIDDEN_TAG_CLASS = "git-source-hidden-count"
HIDDEN_TAG_TEXT = "hidden on"
#: The checkbox's aria-label template — the ONLY place the full
#: source value appears (setAttribute, never innerHTML).
ARIA_LABEL_TEMPLATE = "`Index hidden folders for ${kindLabel} source: ${value}`"
#: The §7.4 lifecycle strings (pinned verbatim).
REVERT_STATE = "box.checked = s.include_hidden === true"
NETWORK_MESSAGE = "Could not reach the server — the setting was not changed."
PATCH_BODY = "JSON.stringify({ include_hidden: wanted })"
def _text(path: Path) -> str:
return path.read_text(encoding="utf-8")
def _js() -> str:
return _text(JS)
def _css() -> str:
return _text(CSS)
def _fn(js: str, name: str) -> str:
"""The source of a (possibly async) top-level function via
balanced-brace counting (the test_source_ignore_paths.py helper)."""
for prefix in ("async function ", "function "):
start = js.find(f"{prefix}{name}(")
if start != -1:
depth = 0
for i in range(js.find("{", start), len(js)):
if js[i] == "{":
depth += 1
elif js[i] == "}":
depth -= 1
if depth == 0:
return js[start : i + 1]
raise AssertionError(f"unbalanced braces in {name}()")
raise AssertionError(f"{name}() must exist in git-sources.js")
def _css_rule(css: str, selector: str) -> str:
"""The declarations of a simple one-line-opening rule (comments
stripped first — a house comment may legally carry braces)."""
clean = re.sub(r"/\*.*?\*/", "", css, flags=re.S)
start = clean.find(f"{selector} {{")
assert start != -1, f"missing rule {selector} in styles.css"
depth = 0
for i in range(clean.find("{", start), len(clean)):
if clean[i] == "{":
depth += 1
elif clean[i] == "}":
depth -= 1
if depth == 0:
return clean[start : i + 1]
raise AssertionError(f"unbalanced braces in {selector}")
def _python_class_field(class_name: str) -> str:
"""The single bool field name declared on a Pydantic class in
``app/schemas.py`` (``bool`` or ``bool | None`` — regex-parsed,
no import: the pin is on the source text, so a rename breaks it
without a schema load)."""
schemas = _text(SCHEMAS)
m = re.search(
rf"class {class_name}\(BaseModel\):.*?(?=\nclass |\Z)", schemas, re.S
)
assert m, f"class {class_name} must exist in app/schemas.py"
fields = re.findall(
r"^\s*(\w+):\s*bool(?:\s*\|\s*None)?\s*(?==|$)", m.group(0), re.M
)
assert len(fields) == 1, f"{class_name} must declare exactly one bool field"
return fields[0]
# ---------- the row wiring in makeRow ----------
def test_row_checkbox_is_built_in_the_stored_row_branch() -> None:
"""makeRow: the Hidden checkbox is created ONLY in the ``s.id``
branch (A3 — env-fallback rows get the "from .env" tag, no
checkbox). It is a NATIVE labeled checkbox: class
git-source-hidden-box, type=checkbox, the visible "Hidden" text,
``checked`` from the SERVER row (``s.include_hidden === true`` —
never a prior local flip, §7.4), and the aria-label template is
the ONLY place ``value`` appears (setAttribute — never
innerHTML). Its change handler runs toggleHidden."""
make = _fn(_js(), "makeRow")
branch_i = make.find("if (s.id) {")
else_i = make.find("} else {", branch_i)
assert branch_i != -1 and else_i > branch_i
box_i = make.find('const hiddenBox = document.createElement("input")', branch_i)
assert -1 < box_i < else_i, "the checkbox is created in the s.id branch (A3)"
assert 'hiddenBox.type = "checkbox"' in make, "a native checkbox"
assert f"hiddenBox.className = \"{HIDDEN_BOX_CLASS}\"" in make
assert "hiddenBox.checked = s.include_hidden === true" in make, (
"checked state comes ONLY from the server row"
)
assert ARIA_LABEL_TEMPLATE in make, "the aria-label template (the only value site)"
assert 'hiddenBox.setAttribute(' in make, "the label is set via setAttribute"
assert "hiddenBox.innerHTML" not in _js(), "XSS contract: no innerHTML on the box"
assert "hiddenLabel.innerHTML" not in _js(), "XSS contract: no innerHTML on the label"
assert 'document.createTextNode("Hidden")' in make, (
"the visible text label — never aria-label-only (WCAG)"
)
assert "hiddenLabel.append(hiddenBox, document.createTextNode(\"Hidden\"))" in make
assert 'hiddenBox.addEventListener("change", () => toggleHidden(s, hiddenBox))' in make
# The env-fallback branch (else) has no checkbox.
else_slice = make[else_i : make.find("tr.appendChild(actTd)", else_i)]
assert HIDDEN_BOX_CLASS not in else_slice and "toggleHidden" not in else_slice, (
"env-fallback rows get no checkbox (A3)"
)
def test_row_dom_order_is_hidden_ignore_remove() -> None:
"""makeRow: the append order in the actions cell is Hidden ·
Ignore paths · Remove (A5 — the toggle sits LEFT of the "Ignore
paths" button; Remove stays last)."""
make = _fn(_js(), "makeRow")
branch_i = make.find("if (s.id) {")
else_i = make.find("} else {", branch_i)
hidden_append = make.find("actTd.appendChild(hiddenLabel)", branch_i)
ignore_i = make.find('const ignoreBtn = document.createElement("button")', branch_i)
ignore_append = make.find("actTd.appendChild(ignoreBtn)", branch_i)
remove_append = make.find("actTd.appendChild(btn)", branch_i)
assert -1 < hidden_append < ignore_i, "the hidden label precedes the ignore button"
assert -1 < ignore_append < remove_append, "Remove stays last"
assert remove_append < else_i, "all three controls are in the s.id branch"
def test_hidden_on_tag_renders_text_in_the_source_cell() -> None:
"""makeRow: a stored row with the flag ON gets the "hidden on"
tag in the SOURCE cell (urlTd — the .git-source-ignore-count
idiom: TEXT, never color alone, WCAG 1.4.1), appended after the
location <code>; the tag is the state copy (pinned verbatim)."""
make = _fn(_js(), "makeRow")
assert "s.id && s.include_hidden === true" in make, (
"the tag only for stored rows with the flag on"
)
assert f"hiddenTag.className = \"{HIDDEN_TAG_CLASS}\"" in make
assert f"hiddenTag.textContent = \"{HIDDEN_TAG_TEXT}\"" in make, (
"the tag copy is TEXT — never color alone"
)
code_i = make.find("urlTd.append(badge, code)")
append_i = make.find("urlTd.append(hiddenTag)", code_i)
assert -1 < code_i < append_i, "the tag is appended to the source cell"
# ---------- the JS lifecycle (toggleHidden) ----------
def test_toggle_sends_patch_and_disables_the_box_first() -> None:
"""toggleHidden: the box disables BEFORE the PATCH goes out
(no double-flip — §7.4); the request is a PATCH to
/api/git-sources/{id} whose body is ``{"include_hidden": …}``
ONLY (the row's ignore list is untouched — task 03's optional
field)."""
body = _fn(_js(), "toggleHidden")
disable_i = body.find("box.disabled = true")
fetch_i = body.find("`/api/git-sources/${s.id}`")
method_i = body.find('method: "PATCH"', fetch_i)
body_i = body.find(PATCH_BODY, method_i)
assert -1 < disable_i < fetch_i < method_i < body_i, (
"disable → PATCH {include_hidden} (and nothing else)"
)
assert "ignore_paths" not in body, "the PATCH body never carries the ignore list"
def test_toggle_success_clears_reloads_and_announces_last() -> None:
"""toggleHidden 200: the error line clears, ``loadSources()``
AWAITS (the row re-renders from the server — the "hidden on" tag
lands), and the confirmation is the LAST announcement (the
reload's "N sources listed." lands first — the phase-89 order)."""
body = _fn(_js(), "toggleHidden")
ok_i = body.find("if (r.ok)")
hide_i = body.find("hideHiddenError()", ok_i)
reload_i = body.find("await loadSources()", ok_i)
announce_i = body.find("announce(`Hidden folders", reload_i)
assert -1 < ok_i < hide_i < reload_i < announce_i, (
"200: clear error → await loadSources → announce (LAST)"
)
assert "for ${value}.`)" in body[announce_i:], "the confirmation names the source"
assert "enabled" in body[announce_i:] and "disabled" in body[announce_i:], (
"the confirmation states the direction of the flip"
)
def test_toggle_failure_reverts_the_box_and_shows_the_page_alert() -> None:
"""toggleHidden failure: non-2xx → the server detail (apiDetail)
into the page-level alert line + the box REVERTS to the server
state + re-enables; network failure → the fixed reachable? line,
same revert. The UI never claims a state the server didn't save
(PLAN §7.4). The revert + re-enable happen on BOTH failure
branches (exactly twice each in the function)."""
body = _fn(_js(), "toggleHidden")
catch_i = body.find(".catch(")
assert catch_i != -1
fail_slice = body[body.find("if (r.ok)"):catch_i]
assert "await apiDetail(" in fail_slice, "the server detail is apiDetail-extracted"
assert "showHiddenError(" in fail_slice, "the detail lands in the page alert line"
assert REVERT_STATE in fail_slice, "the box reverts to the server state"
assert "box.disabled = false" in fail_slice, "the box re-enables"
net_slice = body[catch_i:]
assert NETWORK_MESSAGE in net_slice, "the fixed network copy"
assert "showHiddenError(" in net_slice
assert REVERT_STATE in net_slice
assert "box.disabled = false" in net_slice
assert body.count(REVERT_STATE) == 2, "revert on BOTH failure branches"
assert body.count("box.disabled = false") == 2, "re-enable on BOTH failure branches"
assert "box.disabled = true" in body[: body.find("if (r.ok)")], (
"the box disables at once, before any outcome"
)
def test_error_line_helpers_and_the_healed_load_clear() -> None:
"""showHiddenError/hideHiddenError drive the page-level
``hiddenErrorEl`` (textContent + hidden); ``loadSources``'s
success path calls hideHiddenError() AFTER hideLoadError() — a
healed list clears the stale line (the phase-89 "happy path
heals the error state" precedent)."""
js = _js()
assert 'const hiddenErrorEl = root.querySelector("#git-sources-hidden-error")' in js, (
"the page-local element grabber (root-scoped, the shell idiom)"
)
show = _fn(js, "showHiddenError")
assert "hiddenErrorEl.textContent = message" in show
assert "hiddenErrorEl.hidden = false" in show
hide = _fn(js, "hideHiddenError")
assert 'hiddenErrorEl.textContent = ""' in hide
assert "hiddenErrorEl.hidden = true" in hide
load = _fn(js, "loadSources")
hide_load_i = load.find("hideLoadError()")
hide_hidden_i = load.find("hideHiddenError()", hide_load_i)
assert -1 < hide_load_i < hide_hidden_i, (
"a healed load clears the hidden-toggle line (after the load-error clear)"
)
# ---------- the cross-file wire contract (single source of truth) ----------
def test_js_field_name_matches_the_python_patch_schema() -> None:
"""The cross-file pin: the JS PATCH body key and the render field
both equal their ``app/schemas.py`` counterparts —
``GitSourcePatchIn.include_hidden`` (the write side) and
``GitSourceRow.include_hidden`` (the read side). A rename on
either side breaks the wire contract and this test."""
js = _js()
body = _fn(js, "toggleHidden")
m = re.search(r"JSON\.stringify\(\{\s*(\w+)\s*:\s*wanted\s*\}\)", body)
assert m, "the PATCH body carries a single {<field>: wanted} object"
js_body_key = m.group(1)
assert js_body_key == _python_class_field("GitSourcePatchIn"), (
"the JS PATCH body key must match GitSourcePatchIn's field"
)
make = _fn(js, "makeRow")
rm = re.search(r"s\.(\w+)\s*===\s*true", make)
assert rm, "makeRow reads the flag off the server row (s.<field> === true)"
js_read_key = rm.group(1)
assert js_read_key == _python_class_field("GitSourceRow"), (
"the render field must match GitSourceRow's field"
)
assert js_body_key == js_read_key == "include_hidden", (
"one field name for the whole wire contract"
)
# ---------- the static shell markup ----------
def test_page_error_line_is_a_role_alert_after_the_table() -> None:
"""index.html: ``#git-sources-hidden-error`` is a ``<p>`` reusing
the existing ``.git-source-error`` class, ``role="alert"`` +
hidden by default, INSIDE ``#view-git-sources`` and AFTER
``#git-sources-table-wrap`` in source order (the checkbox is a
table-cell control, so its failure announces at page level — the
ignore dialog carries its own error INSIDE the modal). It
appears exactly once."""
html = _text(SHELL_HTML)
tag = re.search(r'<p[^>]*id="git-sources-hidden-error"[^>]*>', html)
assert tag, "#git-sources-hidden-error must be a real <p>"
open_tag = tag.group(0)
for attr in ('class="git-source-error"', 'role="alert"', "hidden"):
assert attr in open_tag, f"the error line carries {attr}"
view_i = html.find('id="view-git-sources"')
wrap_i = html.find('id="git-sources-table-wrap"')
err_i = html.find('id="git-sources-hidden-error"')
next_i = html.find('id="view-history"')
assert -1 < view_i < wrap_i < err_i < next_i, (
"the line sits inside #view-git-sources, after #git-sources-table-wrap"
)
assert html.count('id="git-sources-hidden-error"') == 1, (
"the id is unique in the shell"
)
# ---------- styles.css ----------
def test_hidden_checkbox_css_rules_exist_with_accent_color() -> None:
"""styles.css carries the phase-105 family: .git-source-hidden
(inline-flex, 44px hit height matching the action buttons, the
house ink), the checkbox (sized, ``accent-color: var(--brand)``),
the :disabled wait state (the .git-source-remove:disabled
idiom), and the "hidden on" tag (text + a distinct background —
never color alone). The checkbox comment RECORDS the
verified checked-state contrast (house style)."""
css = _css()
label = _css_rule(css, ".git-source-hidden")
assert "display: inline-flex" in label
assert "height: 44px" in label, "the ~44px hit height matches the action buttons"
assert "var(--ink)" in label
box = _css_rule(css, '.git-source-hidden input[type="checkbox"]')
assert "accent-color: var(--brand)" in box, "the --brand checkbox fill"
disabled = _css_rule(css, ".git-source-hidden:disabled")
assert "opacity" in disabled and "cursor: wait" in disabled, (
"the .git-source-remove:disabled idiom"
)
tag = _css_rule(css, ".git-source-hidden-count")
assert "var(--ink)" in tag and "var(--bg)" in tag, (
"text + a distinct background (never color alone)"
)
assert "url(http" not in css and "@import url(" not in css, (
"no CDN (AGENTS.md rule 6)"
)
def test_checkbox_contrast_is_verified_and_recorded() -> None:
"""House style: the checked-state contrast of the native widget
is VERIFIED and the ratio RECORDED in the comment above the
checkbox rule (the executor note the task template carries):
the --bg-on---brand house pairing (5.2:1) on the built-in theme,
and the theme tab's AA gate (4.5:1) for every saved palette."""
css = _css()
rule_i = css.find('.git-source-hidden input[type="checkbox"] {')
assert rule_i != -1
header = css[css.rfind("/*", 0, rule_i):rule_i]
assert "VERIFIED" in header, "the contrast is verified (the executor note)"
assert "5.2:1" in header, "the built-in --bg-on---brand ratio is recorded"
assert "4.5:1" in header, "the AA gate for themed palettes is recorded"
# ---------- the module docstring ----------
def test_module_docstring_carries_the_phase_105_contract() -> None:
"""The git-sources.js module docstring gained the phase-105
entry: the per-row Hidden checkbox (makeRow) →
PATCH {include_hidden} (task 03's optional field) →
loadSources + announce; failure reverts the box +
#git-sources-hidden-error (role=alert); env-fallback rows get
no checkbox (A3)."""
doc = _js().split("*/", 2)[0] # the module docstring (first block)
for frag in (
"Phase 105 (task 05)",
"toggleHidden",
"{ include_hidden }",
"#git-sources-hidden-error",
"env-fallback rows get NO checkbox",
"REVERTS to the server state",
"LAST announcement",
):
assert frag in doc, f"the module docstring lost: {frag!r}"