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`.
437 lines
20 KiB
Python
437 lines
20 KiB
Python
"""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}"
|