**Phase 194 — final verification pass: all green.**
**Verified (all 5 tasks already complete; nothing re-implemented needed)**
- `app/rag/importer.py`: `_prescan_pending` + `_pending_hash_stmt` (scalar `documents.file_hash`, own session, ONE `asyncio.to_thread` span, REPLACES the stats pre-walk), `on_scan_progress`, `progress` pair = pending position, `0` branch = byte-identical phase-193 walk.
- `app/config.py`/`settings_catalog.py`/`.env.example`: `sync_pending_count=1` + validator; `app/api/sync.py`: `"scan"` phase, `scan_done`/`scan_total` (additive after `files_total`, kept at every terminal), forced/throttled RSS split; `frontend/assets/sources.js`: `fmtSyncPhaseLabel` scan branch first-arm, `fmtSyncLabel` untouched.
**Tests / lint / coverage (exact commands)**
- `uv run pytest --cov=app --cov-report=term-missing` → **4835 passed**, `app/` 99% (7240 stmts / 16 miss), exit 0
- `uv run pytest tests/e2e/test_sync_pending_count.py -v --no-cov` → **8 passed in 150.81s** (phase suite, isolated)
- 14 sync-lineage E2E suites, one pytest process each (`test_sync_remaining_count`, `…_chunk_progress`, `…_progress_box`, `…_peak_memory_visible`, `…_label_mobile`, `…_upload_progress`, `test_archive_upload_sources`, `…_image_progress`, `…_summary_visibility`, `…_status_freshness`, `test_smoke`, `test_fast_path_deferred_content`, `test_sync_session_reset`, `test_sync_git_freeze`) → **all exit 0**
- `uv run ruff check .` → All checks passed; `uv run pyright` → **0 errors**
**Completion criteria** — all met: `N remaining` = predicted change set (E2E arms 1/2/5, first import counts the walk, no-change → no count part); `scan` phase visible + flips to `import`, scan pair reset/kept at terminals (`test_sync_scan_phase.py`); `BOR_SYNC_PENDING_COUNT=0` walk semantics ("7 remaining" arm) + **negative refuses startup naming the field (verified live by importing `app.main`)**; pre-scan off-loop + cancel lands `cancelled` (integration pins); CLI/upload byte-identical (unedited pins green); payload additive (`_PRE_187_STATUS_KEYS | _MEMORY_KEYS | _SCAN_KEYS`); suite green, coverage >90%, lint/types clean.
**Documentation** — 3 fixes: README sync "States" bullet had an **unclosed parenthesis** (pre-existing, 193/194) and the scan-label "total is 0" clause was ambiguous about a `=0` run (it still counts the walk once the import starts); **AGENTS.md rule 17 had a stray `)`** (pre-existing from phase 191). Also verified consistency both directions for all 87 settings ↔ README ↔ `.env.example` ↔ settings catalog, the pages table ↔ `app/main.py` routes, no stale `BOR_` names, and README/AGENTS claims through phase 194.
**Notes/deviations** — no live-infra changes (prod sync knob ships default-on, no deployment touched); no git index/history commands run (all changes left in the working tree for the harness); commit criterion left to the harness as instructed.
**Next pending phase:** none — `.agents/phases/todo/` holds only `194_sync_pending_count`, which is now ready to move to `complete/`.
545 lines
24 KiB
Python
545 lines
24 KiB
Python
"""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 <name>`` (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 <name>``."""
|
|
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] == ""
|