"""Phase 194, task 03 — the Sources page renders the scan as a scan. The owner report the whole phase exists for (2026-10-19): *"1216 remaining"* on a ~10,000-file KB that had ~10 files to sync. Task 01 moved the server's ``done``/``total`` to the pre-scan's predicted change set and task 02 exposed the scan as a phase (``phase: "scan"`` + ``scan_done`` / ``scan_total``). This module pins the UI half (D4, owner-confirmed A2): ``fmtSyncPhaseLabel`` gains ONE explicit ``"scan"`` branch that says ``Scanning…`` and puts the scan's own ``(n/total)`` in the SAME count slot the import phase's ``N remaining`` uses — so the walk position surviving as an ``n/m`` is honest, because the word in front of it is "Scanning" and never "Syncing" (TODO L6's objection was the sync count wearing the total's shape). Pinned here (source markers, plus one node run of the REAL label path): * the branch — ``status.phase === "scan"`` is in the decision tree BEFORE ``overview`` / ``summaries`` / the fall-through, its main text is exactly ``Scanning…``, and it never reads ``current_file`` (null in the phase), the file pair, the chunk pair, the image pair or the summaries pair; * the shape — the count part is ``(${status.scan_done}/${status.scan_total})`` landing on the ``count`` slot (``setSyncLabel``'s hidden-toggle + the A4 untruncated button-title / ``#sync-progress`` placement carry it with no new element), with the phase-64 ``total === 0`` guard (the empty KB and the scan's own first frame render the bare ``Scanning…``) and no chunk/image part (the scan reads no file); * the pass-through — both sync status call sites (the poll tick, the load-time re-attach) forward the WHOLE status object, which is what carries the scan pair to the wrapper (the summaries / chunk / image pattern: ``phase`` selects which pair renders, no field is re-listed); * the UNTOUCHED set — ``fmtSyncLabel``'s ``N remaining`` expression verbatim (the phase-193 pin), the chunk / image / summaries part builders, ``setSyncLabel``'s composed full string, ``fmtSyncResult`` and ``fmtSyncPeakClause`` (the phase-187 trailing clause lands on the scan label's line unchanged, and stays a terminal-only clause); * the docstrings — the decision-tree paragraph names the new branch, narrows the fallback to the null prelude + ``"import"``, and keeps the phase-193 ``N remaining`` clause. The owner-visible arms (the count-down over a real incremental sync, the scan label in a real browser) live in ``tests/e2e/test_sync_pending_count.py`` (task 05); the stale pinned suites retarget in task 04. """ from __future__ import annotations import json import shutil import subprocess from pathlib import Path import pytest #: The node behavior check needs node (the test-document-viewer pattern). HAVE_NODE = shutil.which("node") is not None ROOT = Path(__file__).resolve().parents[2] SOURCES_JS = ROOT / "frontend" / "assets" / "sources.js" def _js() -> str: return SOURCES_JS.read_text(encoding="utf-8") def _fn(js: str, name: str) -> str: """The source of the first ``function `` (brace balanced — the house pin pattern from tests/unit/test_frontend_sync_upload.py: the functions live inside ``mount(root)``, so the closing brace is indented).""" fn = js.find(f"function {name}") assert fn != -1, f"{name} must be defined in sources.js" open_idx = js.find("{", fn) depth = 0 for i in range(open_idx, len(js)): c = js[i] if c == "{": depth += 1 elif c == "}": depth -= 1 if depth == 0: return js[fn : i + 1] raise AssertionError(f"unbalanced braces in {name}") def _doc_above(js: str, name: str) -> str: """The block comment immediately above ``function ``.""" i = js.find(f"function {name}") assert i != -1, f"{name} must be defined in sources.js" start = js.rfind("/*", 0, i) assert start != -1, f"{name} must carry a docstring" end = js.find("*/", start) assert start < end < i, f"{name}'s docstring must sit above the function" return js[start:end] def _scan_slice(js: str) -> str: """The ``"scan"`` branch of ``fmtSyncPhaseLabel`` — from its ``if`` to the ``overview`` branch that follows it (every shape pin below reads ONLY this slice, so a shape that leaked into another branch is caught and a branch that moved is caught).""" body = _fn(js, "fmtSyncPhaseLabel") start = body.find('status.phase === "scan"') assert start != -1, 'the "scan" branch is missing from fmtSyncPhaseLabel' end = body.find('status.phase === "overview"', start) assert end != -1, "the overview branch must follow the scan branch" return body[start:end] # ---------- the branch ---------- def test_scan_branch_is_the_first_arm_of_the_decision_tree() -> None: """The wrapper gains ONE explicit ``"scan"`` branch (D4): the phase comparison sits BEFORE ``overview``, ``summaries`` and the ``fmtSyncLabel`` fall-through (the machine order is ``null`` → ``"scan"`` → ``"import"`` → ``"overview"`` → ``"summaries"``, and keeping the arms in that order is what leaves every pre-existing branch slice byte-identical), and the main text is exactly ``Scanning…`` with the ellipsis the other running labels use.""" js = _js() body = _fn(js, "fmtSyncPhaseLabel") i_scan = body.find('status.phase === "scan"') i_ov = body.find('status.phase === "overview"') i_sum = body.find('status.phase === "summaries"') i_fall = body.find("fmtSyncLabel(", i_sum) assert -1 < i_scan < i_ov < i_sum < i_fall, ( "the scan branch must be the first arm, ahead of overview / " "summaries / the null-prelude + import fall-through" ) assert 'main: "Scanning\u2026"' in _scan_slice(js), ( "the main text is the bare Scanning label — no file" ) def test_scan_branch_reads_the_scan_pair_and_nothing_else() -> None: """The branch consumes the scan pair the way it already consumes the summaries pair (``phase`` selects which pair renders). It NEVER reads ``current_file`` — the runner keeps it null through the phase, because the scan names no file — nor the file pair, the chunk pair, the image pair or the summaries pair (a scan embeds no chunk and describes no image, so those fields carry nothing meaningful here).""" scan = _scan_slice(_js()) assert "status.scan_done" in scan and "status.scan_total" in scan, ( "the count part is the status's own scan pair" ) for field in ( "current_file", "files_done", "files_total", "chunks_total", "current_chunk", "images_total", "images_done", "summaries_total", "summaries_done", "current_summary", ): assert field not in scan, ( f'the "scan" branch must not read {field} — that field is the ' "import/overview/summaries phase's own position" ) def test_scan_branch_emits_no_chunk_and_no_image_part() -> None: """The scan names no file, so its parts are the main text + the count only: ``chunk: ""`` and ``image: ""`` (the writer hides both spans and keeps them out of the composed full string — the overview branch's shape, phase 98/152).""" scan = _scan_slice(_js()) assert 'chunk: ""' in scan, "the scan label carries no chunk part" assert 'image: ""' in scan, "the scan label carries no image part" # ---------- the (n/total) count part ---------- def test_scan_count_part_is_the_pair_in_the_count_slot() -> None: """The count part is the exact ``(${status.scan_done}/${status.scan_total})`` template — a plain ``(n/total)``, no thousands separator, no suffix — and it lands on the ``count`` key, i.e. the SAME slot the import phase's ``N remaining`` uses (``setSyncLabel``'s ``.sync-label-count`` span + the composed full string), so the count stays pinned on a phone and the A4 untruncated label (button title + ``#sync-progress``) carries ``Scanning… (n/total)`` with no new element and no new live region.""" scan = _scan_slice(_js()) assert "`(${status.scan_done}/${status.scan_total})`" in scan, ( "the exact (n/total) shape over the scan pair" ) i_count = scan.find("count:") i_template = scan.find("`(${status.scan_done}/${status.scan_total})`") assert -1 < i_count < i_template, "the pair renders INTO the count slot" assert "remaining" not in scan, ( "the scan never says remaining — that part belongs to fmtSyncLabel" ) for helper in ("toLocaleString", ".toFixed"): assert helper not in scan, "plain integers, the house label style" def test_scan_count_part_hides_while_the_total_is_zero() -> None: """The phase-64 ``total === 0`` contract, extended to the scan: while the scan has not reported its denominator (and for the degenerate empty KB, where the importer fires no scan callback at all) the count part is the empty string — the bare ``Scanning…`` renders, never ``(0/0)``. The guard precedes the template and the false arm is the empty string.""" scan = _scan_slice(_js()) i_guard = scan.find("status.scan_total > 0") i_template = scan.find("`(${status.scan_done}/${status.scan_total})`") i_else = scan.find(': ""', i_template) assert -1 < i_guard < i_template < i_else, ( "scan_total > 0 ? (n/total) : \"\" — the phase-64 no-count-part guard" ) # ---------- the caller pass-through ---------- def test_poll_tick_forwards_the_whole_status_so_the_scan_pair_rides() -> None: """The running-sync branch of the poll tick passes the WHOLE status object as ``enterSyncRunningState``'s fifth argument — that object IS the pass-through for the scan pair, exactly like the summaries / chunk / image pairs (the call site names it). A status with ``phase: "scan"`` therefore reaches the wrapper with its ``scan_done`` / ``scan_total`` intact.""" js = _js() start = js.find("function startSyncPolling") assert start != -1 tick = js[start : js.find("async function startSync", start)] i_run = tick.find('syncStatus.state === "running"') assert i_run != -1 # the call-site comment sits directly above the branch — it is part of # the pass-through's documentation, so the slice starts there branch = tick[tick.rfind("// 1. sync running", 0, i_run) : tick.find( 'uploadStatus && uploadStatus.state === "running"', i_run )] assert ( '"sync", syncStatus.current_file, syncStatus.files_done, syncStatus.files_total' in branch ), "the pre-phase positional args are unchanged" i_args = branch.find("syncStatus.files_total") i_pass = branch.find("syncStatus\n", i_args) assert i_pass != -1, "the WHOLE syncStatus object rides in as the 5th arg" assert "scan_done" in branch and "scan_total" in branch, ( "the call-site comment names the scan pair riding the status (phase 194)" ) def test_load_time_reattach_forwards_the_whole_status_too() -> None: """The load-time re-attach is the other sync call site: a reload during the pre-scan re-enters running state with the WHOLE status, so the re-attached label reads ``Scanning… (n/total)`` (the never-stale contract — phase 98's mid-summaries reload, now also mid-scan).""" js = _js() body = _fn(js, "initSyncButton") assert ( '"sync", status.current_file, status.files_done, status.files_total' in body ), "the pre-phase positional args are unchanged" i_args = body.find("status.files_total") i_pass = body.find("status\n", i_args) assert i_pass != -1, "the WHOLE status object rides in as the 5th arg" doc = _doc_above(js, "initSyncButton") assert "phase 194" in doc and "Scanning" in doc, ( "the re-attach docstring names the scan label it can re-enter (194)" ) def test_running_entry_hands_the_status_to_the_phase_wrapper() -> None: """``enterSyncRunningState`` keeps the phase-aware split untouched: sync + status → ``fmtSyncPhaseLabel(status)`` (the wrapper that now owns the scan branch), everything else → the bare ``fmtSyncLabel`` parts, and the parts still go to the ONE writer with the running flag (so the scan label lands on the button title + the box).""" entry = _fn(_js(), "enterSyncRunningState") assert "fmtSyncPhaseLabel(status)" in entry assert 'kind === "sync" && status' in entry assert "setSyncLabel(main, count, chunk, true, image)" in entry # ---------- the UNTOUCHED set ---------- def test_fmt_sync_label_n_remaining_arithmetic_is_byte_identical() -> None: """The phase-193 count-down builder is UNTOUCHED by this task — the server-side semantic change (the pair becomes the pending position) flows through the SAME expression. The exact line, both guards and the plain-integer shape, plus the absence of anything scan-shaped in this builder.""" bare = _fn(_js(), "fmtSyncLabel") assert ( "const count = total > 0 && total - done > 0" " ? `${total - done} remaining` : \"\";" ) in bare, "the phase-193 count expression, byte for byte" assert 'const prefix = kind === "upload" ? "Importing" : "Syncing\u2026";' in bare assert "return { main, count }" in bare for word in ("scan", "chunk", "image", "summaries", "phase"): assert word not in bare, f"fmtSyncLabel stays free of {word!r}" def test_other_phase_labels_and_the_writers_are_untouched() -> None: """The regression pins for everything the scan branch could have disturbed: the overview copy, the summaries folder + ``(n/m)`` position, the import fall-through's chunk + image parts, the single writer's signature + composed full string, the terminal counts line (no running count part, no scan word) and the phase-187 peak clause (still a terminal-only trailing clause, null renders nothing).""" js = _js() phase = _fn(js, "fmtSyncPhaseLabel") assert ( 'return { main: "Writing KB overview\u2026", count: "", chunk: "", image: "" };' in phase ) assert "`Summarizing folders\u2026${folder}`" in phase assert ( "count: `(${status.summaries_done}/${status.summaries_total})`" in phase ), "the summaries folder position keeps its (n/m)" assert ( 'const { main, count } = fmtSyncLabel(\n' ' "sync", status.current_file, status.files_done, status.files_total\n' " );" in phase ), "the import fall-through still calls the bare builder unchanged" assert "`\u00b7 chunk ${status.current_chunk}/${status.chunks_total}`" in phase assert "`\u00b7 image ${status.images_done}/${status.images_total}`" in phase assert "return { main, count, chunk, image };" in phase writer = _fn(js, "setSyncLabel") assert ( 'function setSyncLabel(mainText, countText = "", chunkText = "",' ' running = false, imageText = "")' in writer ) assert "syncLabelCount.textContent = countText" in writer assert "syncLabelCount.hidden = false" in writer assert ( "const full =\n" " mainText +\n" " (countText ? ` ${countText}` : \"\") +\n" " (chunkText ? ` ${chunkText}` : \"\") +\n" " (imageText ? ` ${imageText}` : \"\");" in writer ), "the A4 untruncated label is still main + count + chunk + image" assert "if (running) setSyncProgress(full);" in writer assert "syncBtn.title = full" in writer result = _fn(js, "fmtSyncResult") for word in ("remaining", "Scanning", "scan"): assert word not in result, f"the terminal counts line stays {word!r}-free" peak = _fn(js, "fmtSyncPeakClause") assert "return ` \u00b7 peak ${peak} MB`;" in peak assert "status.rss_peak_mb ?? status.rss_mb" in peak assert "Scanning" not in peak, "the clause is terminal-only, unchanged" # ---------- the docstrings ---------- def test_phase_label_docstring_names_the_branch_and_narrows_the_fallback() -> None: """The decision-tree paragraph above the wrapper names the new arm and narrows the fall-through to the two states that remain, keeps the phase-193 ``N remaining`` clause, states the ``total === 0`` guard, and says WHY an ``(n/total)`` is honest here (TODO L6: the objection was the sync count wearing the total's shape — this label says Scanning, never Syncing).""" doc = _doc_above(_js(), "fmtSyncPhaseLabel") assert '"scan"' in doc and "Scanning\u2026" in doc, "the new branch is named" assert "scan_done" in doc and "scan_total" in doc, "the pair is named" assert "scan_total === 0" in doc, "the no-count-part guard is documented" assert "ONLY the two" in doc and '"import"' in doc, "the fallback is narrowed" assert "`N remaining`" in doc and "N = total - done" in doc, ( "the phase-193 count-down clause survives the rewrite" ) assert "never" in doc and '"Syncing"' in doc, ( "the honesty clause: Scanning, never Syncing (TODO L6)" ) assert "A4" in doc, "the untruncated placement is still documented" def test_module_and_section_headers_document_the_scan_label() -> None: """The module header carries the phase-194 paragraph (the count is the WORK, the scan says so), and the sync section's decision tree names the ``"scan"`` arm with its ``(n/total)`` — the header the wrapper's docstring points at.""" js = _js() header = js[: js.find("import { fetchIsAdmin }")] assert "Phase 194 (task 03)" in header, "the module header names the phase" assert "Scanning\u2026 (n/total)" in header, "the module header names the label" marker = js.find("Sync sources button (Sources page only)") assert marker != -1 section = js[marker : js.find("const syncBtn", marker)] assert "Phase 194 task 03" in section, "the section header names the phase" assert "Scanning\u2026 (n/total)" in section, "the decision tree names the label" assert "scan_done" in section, "the pair the WHOLE status carries is named" # ---------- the REAL label path under node ---------- #: The stub element refs (the test-document-viewer pattern; the same #: shapes tests/unit/test_frontend_sync_upload.py drives). The label #: writer, the box writer and the cancel control are the REAL functions. _NODE_HEAD = """ const syncLabelMain = { textContent: "" }; const syncLabelCount = { textContent: "", hidden: true }; const syncLabelChunk = { textContent: "", hidden: true }; const syncProgress = { hidden: true }; const syncProgressText = { textContent: "" }; const syncBtn = { disabled: false, title: "", attrs: {}, setAttribute(k, v) { this.attrs[k] = v; }, removeAttribute(k) { delete this.attrs[k]; }, classes: new Set(), classList: { add(c) { syncBtn.classes.add(c); }, remove(c) { syncBtn.classes.delete(c); }, has(c) { return syncBtn.classes.has(c); }, }, }; const syncIcon = { classes: new Set(), classList: { add(c) { syncIcon.classes.add(c); }, remove(c) { syncIcon.classes.delete(c); }, has(c) { return syncIcon.classes.has(c); }, }, }; const syncCancelBtn = { hidden: true, disabled: true, attrs: {}, setAttribute(k, v) { this.attrs[k] = v; }, removeAttribute(k) { delete this.attrs[k]; }, }; """ _NODE_TAIL = """ const show = (tag) => { console.log(JSON.stringify([ tag, syncLabelMain.textContent, syncLabelCount.textContent, syncLabelCount.hidden, syncBtn.title, syncProgressText.textContent, syncBtn.attrs['aria-label'] || '', ])); }; // 1. the scan, denominator known + the position climbing (the mid-scan poll): enterSyncRunningState("sync", null, 0, 0, { phase: "scan", current_file: null, files_done: 0, files_total: 0, scan_done: 3, scan_total: 9, current_chunk: 0, chunks_total: 0, }); show("scan-3-9"); // 2. the scan's degenerate arm: an empty KB / the scan's own first frame // (0/0) — the phase-64 guard, the bare label with NO count part: enterSyncRunningState("sync", null, 0, 0, { phase: "scan", current_file: null, files_done: 0, files_total: 0, scan_done: 0, scan_total: 0, current_chunk: 0, chunks_total: 0, }); show("scan-0-0"); // 3. the prelude (null phase) is untouched — bare "Syncing\u2026": enterSyncRunningState("sync", null, 0, 0, { phase: null, current_file: null, files_done: 0, files_total: 0, scan_done: 4, scan_total: 9, current_chunk: 0, chunks_total: 0, }); show("prelude"); // 4. the import phase is untouched — the phase-193 count-down + the chunk // part, and the scan pair present on the payload but NOT rendered: enterSyncRunningState("sync", "src/a/b.md", 2, 9, { phase: "import", current_file: "src/a/b.md", files_done: 2, files_total: 9, scan_done: 9, scan_total: 9, current_chunk: 14, chunks_total: 38, }); show("import"); // 5. the overview + summaries labels are untouched: enterSyncRunningState("sync", null, 9, 9, { phase: "overview", current_file: null, files_done: 9, files_total: 9, scan_done: 9, scan_total: 9, current_chunk: 0, chunks_total: 0, }); show("overview"); enterSyncRunningState("sync", null, 9, 9, { phase: "summaries", current_file: null, files_done: 9, files_total: 9, scan_done: 9, scan_total: 9, current_summary: "src/alpha", summaries_done: 2, summaries_total: 3, current_chunk: 0, chunks_total: 0, }); show("summaries"); // 6. the 202/409 click (no status at all) stays bare: enterSyncRunningState("sync", null, 0, 0); show("click"); """ @pytest.mark.skipif(not HAVE_NODE, reason="node not available") def test_scan_label_renders_through_the_real_label_path_under_node() -> None: """Behavior, not just markers: the REAL extracted functions run against stub refs. ``phase: "scan"`` renders main ``Scanning…`` with the count ``(3/9)`` visible, and the SAME string lands on the button title, the box (the sole aria-live region) and the button's accessible name (A4) — while every other phase renders exactly what it rendered before this task.""" js = _js() fns = "\n".join( _fn(js, name) for name in ( "fmtSyncLabel", "fmtSyncPhaseLabel", "setSyncLabel", "setSyncProgress", "showSyncCancelBtn", "hideSyncCancelBtn", "enterSyncRunningState", ) ) out = subprocess.run( ["node", "-e", _NODE_HEAD + fns + _NODE_TAIL], capture_output=True, text=True, timeout=60, ) assert out.returncode == 0, out.stderr rows = {row[0]: row for row in (json.loads(line) for line in out.stdout.splitlines())} main, count, hidden, title, box, aria = rows["scan-3-9"][1:] assert main == "Scanning\u2026", rows["scan-3-9"] assert count == "(3/9)" and hidden is False, rows["scan-3-9"] assert title == "Scanning\u2026 (3/9)" == box == aria, ( f"A4: the untruncated scan label rides title + box + aria: {rows['scan-3-9']}" ) main, count, hidden, title, box, aria = rows["scan-0-0"][1:] assert main == "Scanning\u2026" and count == "" and hidden is True, rows["scan-0-0"] assert title == "Scanning\u2026" == box == aria, ( f"0/0 renders NO count part (the empty-KB guard): {rows['scan-0-0']}" ) # every other state is byte-identical to its pre-phase label assert rows["prelude"][1] == "Syncing\u2026" and rows["prelude"][2] == "" assert rows["prelude"][4] == "Syncing\u2026", "a scan pair never renders in the prelude" assert rows["import"][1] == "Syncing\u2026 src/a/b.md" assert rows["import"][2] == "7 remaining", rows["import"] assert rows["import"][4] == "Syncing\u2026 src/a/b.md 7 remaining \u00b7 chunk 14/38" assert rows["overview"][1] == "Writing KB overview\u2026" and rows["overview"][2] == "" assert rows["summaries"][1] == "Summarizing folders\u2026 src/alpha" assert rows["summaries"][2] == "(2/3)", "the summaries position keeps its (n/m)" assert rows["click"][1] == "Syncing\u2026" and rows["click"][2] == ""