"""Unit: the RAG-page sync button's phase-64 (task 04) live-file contract. The browser behavior is E2E-covered (tests/e2e/test_sync_upload_progress.py, task 06); here we pin the source-level wiring in sources.js, styles.css, and the shell's RAG view markup (index.html — sources.html / git- sources.html folded in, phase 76 task 02) — the fmtSyncLabel contract (both kinds, file present/absent, counts only when total > 0), enterSyncRunningState writing the full untruncated path to the button title + #sync-progress (the sole aria-live region — phase 150 retired the announcer span), the two-job tick decision tree (sync running > upload running > sync success > sync failed > upload success > upload failed > idle; the A3 settle never renders upload counts into #sync-progress), and the load-time re-attach of an in-flight upload run (phase 90: unpack + register only — the bare "Importing…" label, no file) — so a silent regression is caught without a browser. Phase 64 task 05 adds the Sources-page (git-sources.js) upload contract: the "Successfully uploaded — " toast on the 202 (page-local, phase-55 pattern, success-only), the "Processing…" label driven by the 2 s GET /api/git-sources/upload/status poll, the 409 re-attach (no error banner), the poll's terminal decision tree (success → result line + announce + reload, NO second toast; failed → sanitized error banner + reload; idle → defensive restore), the finally's never-restore-while-polling guard (PLAN §7.4), and the boot re-attach branches. Phase 90 re-points the upload contract to UNPACK + REGISTER ONLY: the processing state is the BARE "Processing…" for the whole background run (no current_file, no "(n/m)" counts, no title — the scan's progress moved to the RAG page's Sync button), the button reads exactly "Upload" (the phase-64 scan-suffixed label is gone), the settled result line is "Uploaded — press Sync sources to import it." (fmtUploadResult off the status's {"message": "uploaded"} detail; the nameless variant after a reload / on the 409 re-attach), and the success announce names the next step. Phase 98 (task 02) pins the SYNC job's phase-aware label (D2): fmtSyncPhaseLabel — "overview" → exactly "Writing KB overview…", "summaries" → "Summarizing folders… (n/m)" (the folder part omitted while current_summary is null → the bare "Summarizing folders… (n/m)"), anything else → the byte-identical phase-64 fmtSyncLabel("sync", …) fall-through — plus the status-object threading (the tick's branch 1 + the initSyncButton running re-attach pass the WHOLE status; the upload job and the 202/409 click stay on the bare label — the upload status has no phase) and the A4 title-rule adjustment (the full untruncated label rides the button title for EVERY running label — the file-only rule is gone). Phase 128 (task 01) re-points the label pins to the PARTS shape: the label's string is split into a shrinks-to-fit MAIN part (prefix + file/folder — the part CSS may ellipsize) and a pinned COUNT part ("(n/m)" — NEVER clipped, hidden whenever absent), so on a phone the file path ellipsizes while the position stays on-screen. The builders no longer return one string — they return the parts ({ main, count }, the same decision tree, the same byte-identical copy) — and ONE writer, setSyncLabel(mainText, countText), lands them: mainText → .sync-label-main's textContent, countText → .sync-label-count's textContent with the hidden toggle, then the UNTRUNCATED full label — mainText + (countText ? " " + countText : "") — onto the button title + #sync-progress (A4 — the screen reader hears the whole thing in the box, phase 150; the visible span carries main + count, the space is the CSS gap). Every other label state (idle "Sync sources", settle "Synced HH:MM", the 202/409 click, the re-attach) goes through setSyncLabel(main) — count hidden. Phase 129 (task 04) adds the label's THIRD pinned part — the chunk position: #sync-label ships a hidden .sync-label-chunk span after the count span (the pinned "· chunk n/m" — the in-flight file's embed-batch position, batch granularity: n the 1-based first chunk of the in-flight batch, m the file's total content chunks). The ONLY builder that reads the status's chunk fields is fmtSyncPhaseLabel's import fall-through — "· chunk n/m" renders while chunks_total > 0 and hides in every other state (the prelude, the 202/409 click, the upload run — its status has no chunk fields, overview, summaries — its own (n/m) is its position, idle / settle). setSyncLabel(mainText, countText, chunkText) lands the part under the same present/absent contract as the count part, and the UNTRUNCATED full label — main + " " + count + " " + chunk — rides the button title + #sync-progress (A4 — the in-flight import reads "Syncing… (2/3) · chunk 14/38"). The part keeps the pinned mono CSS (flex 0 0 auto, nowrap, --mono, the rule right after .sync-label-count). A node harness (the test-document-viewer pattern) behaviorally pins the rendered parts + the full title across every label state. Phase 134 (task 02) extends the node harness to the Cancel control: the extracted functions gain the REAL show/hide helpers + the REAL settleSyncButton (step 8 now runs the settle itself — it goes through the writer as before, plus it hides the Cancel control), and every row snapshots the control's hidden/disabled pair — the behavioral contract: shown + enabled in EVERY sync-running state (the entry shows it for kind "sync"), hidden + disabled for the upload run (the upload job is NOT cancellable in this phase) and after the settle (settleSyncButton always hides it). Phase 149 (task 01) adds the `running` flag to the single writer: setSyncLabel(mainText, countText = "", chunkText = "", running = false) also writes the SAME untruncated full label into the detail box (#sync-progress's .sync-progress-text — textContent only) and reveals the box IFF running (the running entry passes true, the settle passes the default false), and owns the button's accessible name (full live label while running, the ship value "Sync sources" restored on every non-running call). The running entry passes `running = true` + adds the `is-running` class (with the spin), and no longer sets aria-label (the writer owns it); the settle removes `is-running` (with the spin). The node harness rows gain the box's text/hidden pair, the button's `is-running` + the icon's `is-spinning` flags, and the button's aria-label — the behavioral contract: box visible + full label + is-running + spinning + the aria-label equal to the full label in EVERY running state, box hidden + the idle flags + the restored aria-label after the settle. Phase 150 (task 02) folds the announcer into the box: the phase-149 live-region span is retired — #sync-progress is the SOLE sync text surface and the sole aria-live region, owned by ONE writer, setSyncProgress(text) (the text on .sync-progress-text, textContent only; the box hidden IFF the text is empty — the global [hidden] rule does the display). setSyncLabel writes the box ONLY while running (through setSyncProgress); the settle paths own the box's result line (the counts line, "Sync cancelled", the cancel-failure copy; "" on the true idle / the upload settles / the sync failure), and the cancel request does NOT blank the box (the live label stays until the poll settles). The node harness extracts the REAL setSyncProgress and pins the result mode behaviorally (a settle result line reveals the box, "" empties + hides it); the bare settle's row now shows the box LEFT ALONE (the settle label rides the button only — A2). Phase 152 (task 03) adds the label's FOURTH part — the vision-image position: fmtSyncPhaseLabel's import fall-through gains "· image n/m" (rendered while images_total > 0 — a PDF's vision span is in flight; n the 1-based image about to be described, m the file's total images, ONE continuous counter across the PDF's three vision spans), the overview/summaries branches return image: "" (the position belongs to the import phase only — the status's image fields are cleared at those transitions, so the branches never read them), and the bare builder stays image-free (the upload label and the 202/409 click stay bare). setSyncLabel(mainText, countText = "", chunkText = "", running = false, imageText = "") appends the image part LAST to the composed full string — so the box, the button title, and the running aria-label all carry "Syncing… (2/3) · chunk 14/38 · image 3/15". The image part gets NO span of its own (the running button is the spinner — its parts are hidden, phase 149): it rides ONLY the composed string, while the chunk part keeps its .sync-label-chunk span; the two positions are distinct and each disappears independently. The node harness gains the two in-flight image rows (both parts at once — the owner's stuck label — and the image-only pictorial PDF) and pins the composed full string byte-for-byte. Phase 154 (task 02) extends the Sources-page upload line with the COMPLETION STAMP (the phase's date-qualified freshness): the settled line reads "Uploaded () — press Sync sources to import it." (the fresh 202 settle, the page-local name) or the nameless "Uploaded — press Sync sources to import it." (the boot re-render — the stamp comes from the SERVER payload, so the line is self-dating even with the name unknown); a running / legacy / partial payload without timestamps renders the byte-identical pre-phase-154 strings (the nameful / nameless "Uploaded … — press Sync sources to import it." lines). The stamp is PRECOMPUTED by the page-local fmtUploadStamp(status) (the per-page duplication idiom — the same A1 date-qualifier as sources.js's fmtSyncTime, duplicated, not shared; the rule's boundary set is pinned in test_sync_button.py on a fixed clock): it reads status.finished_at first (the success state always has it on the server), falls back to status.started_at (a defensive guard — a corrupt finished_at degrades to the started_at stamp rather than to no stamp), and yields "" when both are absent or every ISO is unparseable. Both render sites — the poll's success settle and the boot re-attach — pass fmtUploadStamp(status) as the third argument; the NO server change (the /upload/status payload is untouched — started_at / finished_at already shipped, phase 64). Phase 154 (task 03) pins the BYTE-IDENTITY of the no-stamp fallbacks against the VERBATIM pre-phase-154 strings: the old 2-arg fmtUploadResult (git history) is pinned in this file as a reference implementation (the name renamed, the body byte-for-byte) and run side by side with the real 3-arg function under node — the no-stamp (nameful / nameless) outputs + the legacy 2-arg call are asserted byte-identical to the reference's output, and the reference's output to the verbatim old line strings (assertable, not assumed). Clock-free: `when` is a precomputed argument, so the no-stamp shapes need no clock at all. The /upload/status full-dict pin (tests/integration/test_git_sources_upload.py, the phase-64 precedent) stays UNCHANGED — this phase adds no server keys (the app/ no-touch regression guard). The qualifier's byte-identity (same-day + fallbacks vs the verbatim pre-phase fmtSyncTime) is pinned in test_sync_button.py on the shared fixed clock; the four shapes and the fmtUploadStamp behavior (finished / started / missing / garbage) ride the task-02 pins below. """ from __future__ import annotations import json import re import shutil import subprocess from pathlib import Path import pytest #: The node harness needs node (the test-document-viewer pattern). HAVE_NODE = shutil.which("node") is not None FRONTEND = Path(__file__).resolve().parents[2] / "frontend" SOURCES_JS = FRONTEND / "assets" / "sources.js" STYLES_CSS = FRONTEND / "assets" / "styles.css" # Phase 76 (task 02): both HTML pages are folded into the ONE-document # shell — the pinned comments now live in the RAG / Sources view # sections of index.html. SHELL_HTML = FRONTEND / "index.html" GIT_SOURCES_JS = FRONTEND / "assets" / "git-sources.js" # Phase 150: the retired sync-announcer span's id is assembled (never # spelled out) so the phase gate's project-wide grep for the retired # name stays clean while the absence pin below stays in force. RETIRED_ID = "-".join(("sync", "result")) # built at runtime: 3.12+ folds # adjacent/"+" constant strings at compile time, which would leave the # retired name in the .pyc — the phase gate greps even the bytecode. def _js() -> str: return SOURCES_JS.read_text(encoding="utf-8") def _css() -> str: return STYLES_CSS.read_text(encoding="utf-8") def _html() -> str: return SHELL_HTML.read_text(encoding="utf-8") def _gjs() -> str: return GIT_SOURCES_JS.read_text(encoding="utf-8") def _ghtml() -> str: return SHELL_HTML.read_text(encoding="utf-8") def _gfn(js: str, name: str) -> str: """The source of the first `function ` in git-sources.js (brace balanced — since phase 76 task 02 the functions live inside mount(root), so the closing brace is indented, not line-leading). """ fn = js.find(f"function {name}") assert fn != -1, f"{name} must be defined in git-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 _utick(js: str) -> str: """The upload poll tick inside startUploadPolling — from `const tick = async () => {` to the next function (initUploadStatus), so the whole decision tree is in the slice.""" fn = js.find("function startUploadPolling") assert fn != -1, "startUploadPolling must be defined in git-sources.js" tick = js.find("const tick = async () => {", fn) assert tick != -1, "the tick must live inside startUploadPolling" end = js.find("async function initUploadStatus", tick) assert end != -1, "initUploadStatus must follow startUploadPolling" return js[tick:end] def _usubmit(js: str) -> str: """The upload form's submit handler — from the addEventListener to the next top-level function (focusNewRow), so every branch (202 / 409 / other non-2xx / network / finally) is in the slice.""" start = js.find('uploadFormEl.addEventListener("submit"') assert start != -1, "the upload form must wire a submit handler" end = js.find("function focusNewRow", start) assert end != -1, "focusNewRow must follow the upload handler" return js[start:end] def _fn(js: str, name: str) -> str: """The source of the first `function ` in js (brace balanced — since phase 76 task 02 the functions live inside mount(root), so the closing brace is indented, not line-leading; the house pin pattern from tests/unit/test_sync_button.py).""" 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 _tick(js: str) -> str: """The poll tick inside startSyncPolling — from `const tick = async () => {` to the next function (startSync), so the whole two-job decision tree is in the slice.""" fn = js.find("function startSyncPolling") assert fn != -1, "startSyncPolling must be defined in sources.js" tick = js.find("const tick = async () => {", fn) assert tick != -1, "the tick must live inside startSyncPolling" end = js.find("async function startSync", tick) assert end != -1, "startSync must follow startSyncPolling" return js[tick:end] # ---------- fmtSyncLabel: the live-file label PARTS contract ---------- def test_fmt_sync_label_returns_the_parts_contract() -> None: """fmtSyncLabel(kind, currentFile, done, total) — Phase 128 (task 01): the builder returns the PARTS, not one string. `kind` picks the prefix — "upload" → "Importing" (the background run's word, A3), anything else → "Syncing…". The MAIN part — the prefix, the file appended only when present (sync: the bare prefix shows during clone/pull, before any file is indexed — A4; phase 90: the upload run never carries a file or counts, so its label is always the bare "Importing"); the COUNT part — only when total > 0 (the import has started), NO join space (the space is the CSS gap between the parts — the full string's space is re-joined by setSyncLabel for the title / announcer, A4). File before counts in the full-string order.""" body = _fn(_js(), "fmtSyncLabel") assert "function fmtSyncLabel(kind, currentFile, done, total)" in body assert 'kind === "upload"' in body assert '"Importing"' in body and '"Syncing…"' in body # the upload prefix must come from the kind check (ternary, in order) i_kind = body.find('kind === "upload"') i_importing = body.find('"Importing"') i_syncing = body.find('"Syncing…"') assert -1 < i_kind < i_importing < i_syncing # the MAIN part: the file appended only when present assert "currentFile ?" in body assert "`${prefix} ${currentFile}`" in body # the COUNT part: only when the import has started (total > 0), # no join space (the parts are joined by setSyncLabel, A4) assert "total > 0" in body assert "`(${done}/${total})`" in body assert "` (${done}/${total})`" not in body, ("no space in the count part") i_file = body.find("currentFile ?") i_counts = body.find("total > 0") assert -1 < i_file < i_counts, "the file lands before the counts" # the parts return (the byte-identical full strings hold when # setSyncLabel joins them: "Syncing…", "Syncing… ", # "Syncing… (n/m)", "Importing") assert "return { main, count }" in body def test_set_sync_label_is_the_single_parts_writer() -> None: """setSyncLabel(mainText, countText = "", chunkText = "", running = false, imageText = "") — Phase 128 (task 01) + Phase 129 (task 04) + Phase 152 (task 03): the SINGLE point that writes #sync-label. The MAIN part gets mainText (textContent only — the house rule, no HTML strings); the COUNT part gets countText when present (textContent + hidden=false — the "(n/m)" position is visible, NEVER clipped) and is emptied + hidden=true when absent; the CHUNK part (phase 129) gets chunkText under the SAME contract (present → the pinned "· chunk n/m" is visible, absent → emptied + hidden=true — the part renders only while a file's embed batch is in flight). Then the UNTRUNCATED full label — mainText + (countText ? " " + countText : "") + (chunkText ? " " + chunkText : "") + (imageText ? " " + imageText : "") — rides the button title (A4 — the screen reader hears the whole "Syncing… (2/3) · chunk 14/38 · image 3/15" there, and in the box — see below). The image part (phase 152) gets NO span of its own (the running button is the spinner — its parts are hidden, phase 149): it rides only the composed full string. Phase 149 (task 01): the fourth parameter `running` (the in-flight flag — the running entry passes true, the settle the default false); phase 152 (task 03) adds the fifth — `imageText` (the bare builders and the non-running calls pass nothing, so it defaults "" and the part stays out of the string). Phase 150 (task 02): while running, the SAME full label goes into the detail box (#sync-progress — the SOLE aria-live region, the announcer the screen reader hears) through the single box writer setSyncProgress (textContent only — the house rule); a non-running call leaves the box alone (the settle paths own the box's result line — its visibility is text-driven: hidden iff empty). The button's accessible name is the full label while running / the ship value "Sync sources" otherwise (overwrites/restores only — never removes).""" body = _fn(_js(), "setSyncLabel") # the exact signature, byte-for-byte (concatenated — the literal # is 105 columns, over the line cap): main, count, chunk, the # phase-149 `running` flag (fourth), the phase-152 `imageText` # (fifth — the bare builders + non-running calls pass nothing, # so it defaults to the empty string). expected_sig = ( 'function setSyncLabel(mainText, countText = "", chunkText = "",' ' running = false, imageText = "")' ) assert expected_sig in body, ( "phase 149: the in-flight flag is the fourth parameter; phase" " 152: the image part is the fifth, defaulting to the empty" " string (the part stays out of the composed full string)" ) # the parts: textContent only (the house rule), null-guarded refs assert "syncLabelMain.textContent = mainText" in body assert "syncLabelCount.textContent = countText" in body assert "syncLabelChunk.textContent = chunkText" in body # present count → visible; absent count → hidden, emptied assert "syncLabelCount.hidden = false" in body assert 'syncLabelCount.textContent = ""' in body assert "syncLabelCount.hidden = true" in body i_vis = body.find("syncLabelCount.hidden = false") i_hid = body.find("syncLabelCount.hidden = true") assert -1 < i_vis < i_hid, "the present-count branch precedes the absent one" # present chunk → visible; absent chunk → hidden, emptied (same contract) assert "syncLabelChunk.hidden = false" in body assert 'syncLabelChunk.textContent = ""' in body assert "syncLabelChunk.hidden = true" in body i_cvis = body.find("syncLabelChunk.hidden = false") i_chid = body.find("syncLabelChunk.hidden = true") assert -1 < i_cvis < i_chid, "the present-chunk branch precedes the absent one" # A4: the untruncated full label rides the title + the announcer — # main + " " + count + " " + chunk (the join spaces = the CSS # gaps), bare main when both are absent. assert ( "const full =\n" " mainText +\n" " (countText ? ` ${countText}` : \"\") +\n" " (chunkText ? ` ${chunkText}` : \"\") +\n" " (imageText ? ` ${imageText}` : \"\");" ) in body, ( "the full label is main + ' ' + count + ' ' + chunk + ' ' +" " image — the image part LAST (spaces = the CSS gaps)" ) i_full = body.find("const full") i_title = body.find("syncBtn.title = full") i_box = body.find("if (running) setSyncProgress(full);") i_aria = body.find('syncBtn.setAttribute("aria-label", running ? full : "Sync sources")') assert -1 < i_full < i_title < i_box < i_aria, ( "built once: full → title → the box through the single writer" " (running calls only, phase 150) → the button's accessible" " name (phase 149)" ) assert "syncProgressText" not in body and "syncProgress.hidden" not in body, ( "the box's text + visibility ride setSyncProgress only (phase 150)" ) assert "innerHTML" not in body, "the label path is textContent only" # ---------- enterSyncRunningState: the parts + the A4 writer ---------- def test_fmt_sync_phase_label_is_the_exact_d2_parts_contract() -> None: """fmtSyncPhaseLabel(status) — the SYNC job's phase-aware label (phase 98 task 02, D2; phase 128 task 01: the PARTS; phase 152 task 03: the IMAGE part). "overview" → main exactly "Writing KB overview…" (no folder, no count part); "summaries" → main "Summarizing folders… " + count "(n/m)" — the folder part (one leading space + current_summary) is omitted while current_summary is null (the phase's first poll → the bare "Summarizing folders…" main), and the COUNT IS ALWAYS EMITTED in this phase (the hook always carries a total); anything else (the null prelude, "import") → the phase-64 fmtSyncLabel parts, byte-identical (same builder, same fields, same order). The UPLOAD job never enters here — its status has no phase (D2). Phase 152 (task 03): the import fall-through is the ONLY branch that reads the status's image fields — "· image n/m" renders while images_total > 0 (a PDF's vision span is in flight — n the 1-based image about to be described, m the file's total images) and hides at 0/0; the overview/summaries branches return image: "" and never read the fields (the position belongs to the import phase only — the runner clears it at those transitions).""" body = _fn(_js(), "fmtSyncPhaseLabel") assert "function fmtSyncPhaseLabel(status)" in body # "overview": the exact copy, no folder, no counts. i_ov = body.find('status.phase === "overview"') i_sum = body.find('status.phase === "summaries"') assert -1 < i_ov < i_sum ov = body[i_ov:i_sum] assert 'main: "Writing KB overview…"' in ov assert 'count: ""' in ov, "the overview label carries no count part" assert "current_summary" not in ov and "summaries_done" not in ov, ( "the overview label carries no folder and no counts" ) # "summaries": the exact template — optional folder, counts always, # NO chunk part (the summaries phase's own (n/m) is its position — # the status's chunk fields are cleared at this transition, so the # branch never reads them). i_fall = body.find("fmtSyncLabel(") assert i_sum < i_fall, "the summaries branch precedes the fall-through" sum_ = body[i_sum:i_fall] assert 'status.current_summary ? ` ${status.current_summary}` : ""' in sum_, ( "the folder part — one leading space + the folder, or nothing" ) assert "main: `Summarizing folders…${folder}`" in sum_, ( "the exact D2 main copy (with and without the folder)" ) assert ( "count: `(${status.summaries_done}/${status.summaries_total})`" ) in sum_, "the count part is ALWAYS emitted in the summaries phase" assert "total > 0" not in sum_, ("no conditional — the hook always carries a total") assert 'chunk: ""' in ov, "the overview label carries no chunk part" assert 'chunk: ""' in sum_, ( "the summaries label carries no chunk part (its (n/m) is its position)" ) assert "chunks_total" not in ov, "the overview branch never reads the chunk fields" assert "chunks_total" not in sum_, "the summaries branch never reads the chunk fields" # phase 152 (task 03): the overview/summaries labels carry NO # image part either (the position belongs to the import phase # only) and never read the status's image fields (the runner # clears them at those transitions). assert 'image: ""' in ov, "the overview label carries no image part" assert 'image: ""' in sum_, ( "the summaries label carries no image part (its (n/m) is its position)" ) assert "images_total" not in ov, "the overview branch never reads the image fields" assert "images_total" not in sum_, "the summaries branch never reads the image fields" # anything else (null prelude, "import"): byte-identical to today's # sync label — same builder, same fields, same order — plus the # phase-129 chunk part: "· chunk n/m" ONLY while chunks_total > 0 # (a file's embed batch is in flight — n the 1-based first chunk # of the batch, m the file's total content chunks, batch # granularity), "" otherwise (0/0 before the first batch — the # part hides) + the phase-152 image part: "· image n/m" ONLY # while images_total > 0 (a PDF's vision span is in flight — n # the 1-based image about to be described, m the file's total # images — ONE continuous counter across the PDF's three vision # spans), "" otherwise (0/0 — the part hides). The fall-through # is the ONLY branch that reads the status's chunk + image # fields, and the two parts are independent (both may show, # each disappears on its own 0/0). i_destr = body.find("const { main, count } = fmtSyncLabel(") assert -1 < i_destr < i_fall, "the fall-through destructures the phase-64 parts" fall = body[i_destr:] assert ( 'const { main, count } = fmtSyncLabel(\n' ' "sync", status.current_file, status.files_done, status.files_total\n' " );" ) in fall, "the phase-64 parts builder, unchanged" assert "status.chunks_total > 0" in fall, ( "the chunk part renders only while a batch is in flight" ) assert "`· chunk ${status.current_chunk}/${status.chunks_total}`" in fall i_guard = fall.find("status.chunks_total > 0") i_else = fall.find(": \"\";", i_guard) assert -1 < i_guard < i_else, "0/0 (before the first batch) → the empty chunk part" # phase 152 (task 03): the image part — "· image n/m" ONLY while # images_total > 0, the exact template + the 0/0 → "" guard, and # the return gains the fourth part. assert "status.images_total > 0" in fall, ( "the image part renders only while a vision span is in flight" ) assert "`· image ${status.images_done}/${status.images_total}`" in fall i_iguard = fall.find("status.images_total > 0") i_ielse = fall.find(": \"\";", i_iguard) assert -1 < i_iguard < i_ielse, "0/0 (before the first image) → the empty image part" assert "return { main, count, chunk, image }" in fall # the bare builder (the upload job + the 202/409 click) returns NO # chunk OR image part — those labels stay bare. bare = _fn(_js(), "fmtSyncLabel") assert "chunk" not in bare, "fmtSyncLabel stays chunk-free (upload + the click stay bare)" assert "image" not in bare, "fmtSyncLabel stays image-free (upload + the click stay bare)" def test_enter_running_state_is_phase_aware_and_writes_the_parts() -> None: """The running-state entry keeps the §7.4 never-stale mechanics (disabled, aria-busy, spinning icon, the stale .is-error removed) and — phase 98 (task 02, D2) — is phase-aware for the SYNC job: a sync-kind call carrying the status object takes the fmtSyncPhaseLabel parts, while the upload job (no status — its endpoint has no phase) and the 202/409 click keep the bare fmtSyncLabel parts, byte-identical. Phase 128 (task 01): the entry itself no longer writes the label — it hands the parts to the SINGLE writer (setSyncLabel(main, count, chunk, running, image) — phase 129: the chunk part destructures with a "" default, phase 152: the image part does too), which lands the main part, the pinned count part (hidden when absent), the pinned chunk part (phase 129 — hidden when absent), and the FULL untruncated label on the button title + #sync-progress (A4 — set for EVERY running label; CSS ellipsizes the main part only — phase 152: with the image part appended last when a PDF's vision span is in flight; it gets no span of its own). Phase 150 (task 02): the box write rides in the writer too (setSyncLabel → setSyncProgress, running mode).""" body = _fn(_js(), "enterSyncRunningState") assert ( "function enterSyncRunningState(kind, currentFile, done, total, status)" in body ) assert "syncBtn.disabled = true" in body assert 'syncBtn.setAttribute("aria-busy", "true")' in body assert "syncIcon.classList.add(\"is-spinning\")" in body assert "syncBtn.classList.remove(\"is-error\")" in body # the phase-aware split: sync + status → the D2 parts builder; the # fall-through (upload / the click, no status) is the byte- # identical phase-64 parts builder. i_phase = body.find("fmtSyncPhaseLabel(status)") i_fall = body.find("fmtSyncLabel(kind, currentFile, done, total)") assert 'kind === "sync" && status' in body assert -1 < i_phase < i_fall, "the phase label precedes the fall-through" # the parts are handed to the single writer (A4 lives in it) — # phase 129: the chunk part destructures with a "" default (the # bare builders — the upload job, the 202/409 click — return no # chunk part, so the part stays hidden there); phase 152: the # image part does too (it rides only the composed full string — # no span); phase 149: the # call passes `running = true` (the detail box reveals + the # full-label aria-label ride in the writer). assert 'const { main, count, chunk = "", image = "" } =' in body, ( "the chunk + image parts destructure with the '' defaults" " (bare builders return none)" ) i_set = body.find("setSyncLabel(main, count, chunk, true, image)") assert -1 < i_fall < i_set, "the parts are written after the split (running = true)" assert body.count("setSyncLabel(") == 1, "one writer call per entry" assert "syncBtn.title =" not in body, ("the title write lives in setSyncLabel (A4)") assert "syncProgress" not in body, ( "the box write lives in setSyncLabel/setSyncProgress (phase 150)" ) assert 'syncBtn.setAttribute("aria-label"' not in body, ( "the aria-label write lives in setSyncLabel (phase 149: the full" " live label while running)" ) assert "syncLabel.textContent" not in body, ("no direct span write — the parts") assert "syncBtn.classList.add(\"is-running\")" in body, ( "the icon-only running button class is added with the spin (phase 149)" ) def test_settle_and_idle_labels_go_through_the_parts_writer() -> None: """Every OTHER #sync-label writer — the idle "Sync sources", the success settle "Synced HH:MM" (via settleSyncButton from applySyncSuccess), the failure / upload-terminal "Sync sources" settles — goes through setSyncLabel(main): the count part is hidden (absent), and the label's full string (bare main) rides the button title (A4, as with every label state — phase 150: the settle labels stay on the button, A2). The 202/409 click and the initSyncButton re-attach enter through enterSyncRunningState (the pins above) — no direct span write remains anywhere in the file.""" js = _js() settle = _fn(js, "settleSyncButton") assert "function settleSyncButton(label)" in settle assert "setSyncLabel(label)" in settle, "the settle label goes through the parts writer" assert settle.count("setSyncLabel(") == 1 assert "setSyncLabel(label, true)" not in settle, ( "the settle is NEVER a running state — setSyncLabel leaves the box" " alone (phase 150: the settle paths own the box's result line)" ) assert "syncBtn.classList.remove(\"is-running\")" in settle, ( "the running class is removed with the spin on EVERY settle (phase 149)" ) assert "syncLabel.textContent" not in settle, ("no direct span write — the parts") # the success settle keeps the "Synced HH:MM" copy on the BUTTON # and writes the counts line to the box AFTER the settle (phase # 150, A2: the settle label no longer lands in the live region — # the announcer hears the counts line, the panel never duplicates # the button). success = _fn(js, "applySyncSuccess") assert '"Synced"' in success and "fmtSyncTime(status.finished_at)" in success i_settle = success.find("settleSyncButton(") # Phase 187 (task 03): the clause builder rides the SAME writer call # (the counts line + its trailing " · peak N MB", null rendering no # clause), so the ordering pin reads the new argument list. i_counts = success.find( "setSyncProgress(fmtSyncResult(status.detail) + fmtSyncPeakClause(status))" ) assert -1 < i_settle < i_counts, ( "the counts line (the box's result line) follows the settle label" ) # NO direct #sync-label write remains anywhere in the module. assert "syncLabel.textContent" not in js, ("the parts writer is the single entry point") # ===================================================================== # Phase 129 (task 04) — the label's third part: the chunk position # ===================================================================== def test_sync_label_markup_ships_the_hidden_chunk_part() -> None: """#sync-label (the shell's RAG view) ships the phase-129 THIRD part after the count span — the pinned "· chunk n/m" span (.sync-label-chunk), hidden + empty by default (the same ship contract as the count span: no chunk position at idle — the part renders only while a file's embed batch is in flight).""" html = _html() btn = html[html.find('id="sync-btn"') : html.find("", html.find('id="sync-btn"'))] label = re.search( r']*id="sync-label"[^>]*>' r'Sync sources' r'' r'' r"", btn, ) assert label, ( "the #sync-label parts markup (main + hidden count + hidden chunk) is missing" ) def test_sync_progress_box_markup_ships_under_the_button_row() -> None: """Phase 149 (task 01) / Phase 150 (task 01): #sync-progress — the sync detail box under the button row (the shell's RAG view): between the .page-head block (the button row + the page sub) and #sync-error-banner. Ships hidden (the anonymous-safe/idle state — sources.js reveals it through the single writer while a run is in flight). Phase 150 folded the announcer in: the box is now the SOLE aria-live region for the sync area (role="status" + aria-live="polite") — no longer presentational, and the retired announcer span is gone (one live region, no double announce). It carries one .sync-progress-text span (id-pinned) — the writer's textContent target, the full untruncated live label.""" html = _html() box = re.search( r'', html, ) assert box, "the shell must carry the #sync-progress detail box (phase 149)" # Under the button row, above the error banner: the RAG view's # .page-head block precedes the box, and the box precedes # #sync-error-banner (the retired announcer span is gone). rag = html.find('id="view-rag"') head = html.find('class="page-head"', rag) banner = html.find('id="sync-error-banner"', rag) assert -1 < head < box.start() < banner, ( "the box sits between the RAG view's .page-head block and " "the error banner (directly under the button row)" ) # Ship-hidden (the reveal is run-scoped, in JS); the box IS the # live region now (phase 150) — never presentational/aria-hidden. tag = html[box.start() : html.find(">", box.start())] assert re.search(r"\bhidden\b", tag), "the box ships hidden (idle / anonymous-safe)" assert 'role="status"' in tag, ("the box is the polite live region (phase 150)") assert 'aria-live="polite"' in tag, ("the box is the polite live region (phase 150)") assert 'aria-hidden' not in tag, ("an aria-live region must not be aria-hidden") assert f'id="{RETIRED_ID}"' not in html, ( "the retired announcer span is gone from the shell (phase 150)" ) def test_sync_label_chunk_css_is_the_pinned_part() -> None: """.sync-label-chunk (phase 129 task 04): the same pinned-part language as .sync-label-count — flex 0 0 auto (NEVER clipped), white-space nowrap, the --mono font (a position reads as data) — and it sits right AFTER the count rule (both mono parts read as data).""" css = _css() count_block = re.search(r"\.sync-label-count\s*\{", css) chunk_block = re.search(r"\.sync-label-chunk\s*\{([^}]*)\}", css) assert chunk_block, "styles.css must style .sync-label-chunk" for prop in ("flex: 0 0 auto", "white-space: nowrap", "font-family: var(--mono)"): assert prop in chunk_block.group(1), f".sync-label-chunk must carry {prop!r}" assert count_block and count_block.start() < chunk_block.start(), ( "the chunk rule sits right after the count rule" ) #: The node harness script (the test-document-viewer pattern): the #: REAL extracted label functions run against stub element refs, and #: every label state is snapshotted as a JSON row — #: [tag, title, announcer (the box's text — the sole aria-live #: region, phase 150), main, count, countHidden, chunk, chunkHidden]. #: No JS template literals in the script body (plain strings only — #: the script is assembled by concatenation, no placeholders). _CHUNK_NODE_HEAD = """ const syncLabelMain = { textContent: "" }; const syncLabelCount = { textContent: "", hidden: true }; const syncLabelChunk = { textContent: "", hidden: true }; // Phase 149 (task 01) + Phase 150 (task 02): the detail box — the // SOLE sync text surface and the sole aria-live region (ship-hidden // = the anonymous-safe/idle state); the REAL single writer // (setSyncProgress) mutates it (text + text-driven hidden — visible // IFF the text is non-empty). const syncProgress = { hidden: true }; const syncProgressText = { textContent: "" }; // Phase 149 (task 01): class-tracking stubs — the is-running / // is-spinning travel-together contract is behavioral, not source. 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); }, }, }; // Phase 134 (task 02): the Cancel control — ships hidden + disabled // (the markup's state); the extracted REAL show/hide helpers mutate // it (show also sets the aria-label — the attrs pocket records it). const syncCancelBtn = { hidden: true, disabled: true, attrs: {}, setAttribute(k, v) { this.attrs[k] = v; }, removeAttribute(k) { delete this.attrs[k]; }, }; """ _CHUNK_NODE_TAIL = """ const show = (tag) => { console.log(JSON.stringify([ tag, syncBtn.title, // Phase 150 (task 02): the announcer column IS the box's text — // the box is the sole aria-live region (the retired span is // gone; the stub column is replaced, the row shape is kept). syncProgressText.textContent, syncLabelMain.textContent, syncLabelCount.textContent, syncLabelCount.hidden, syncLabelChunk.textContent, syncLabelChunk.hidden, syncCancelBtn.hidden, syncCancelBtn.disabled, // Phase 149 (task 01): the detail box (text + hidden), the // is-running / is-spinning flags, the button's accessible name. syncProgressText.textContent, syncProgress.hidden, syncBtn.classes.has('is-running'), syncIcon.classes.has('is-spinning'), syncBtn.attrs['aria-label'] || '', ])); }; // 1. import, batch in flight (the task's fixture values, 14/38): enterSyncRunningState("sync", "src/a/b.md", 2, 3, { phase: "import", current_file: "src/a/b.md", files_done: 2, files_total: 3, current_chunk: 14, chunks_total: 38, }); show("import-inflight"); // 1b. Phase 152 (task 03): import, a PDF's vision span in flight // (the owner's stuck label — the last chunk position 125/126 is // STILL on the status + the climbing image position 3/15): the // composed full string carries BOTH parts (chunk keeps its span, // the image part rides the string only — no span): enterSyncRunningState("sync", "src/a/b.md", 2, 3, { phase: "import", current_file: "src/a/b.md", files_done: 2, files_total: 3, current_chunk: 125, chunks_total: 126, images_done: 3, images_total: 15, }); show("import-image-span"); // 1c. Phase 152 (task 03): import, a purely pictorial PDF (no text // chunks — the chunk position never left 0/0) in its vision // span: the image part shows ALONE (the chunk part stays hidden // + empty — the two parts are independent): enterSyncRunningState("sync", "src/a/b.md", 2, 3, { phase: "import", current_file: "src/a/b.md", files_done: 2, files_total: 3, current_chunk: 0, chunks_total: 0, images_done: 7, images_total: 7, }); show("import-image-only"); // 2. import before the first batch (0/0) — the chunk part hides: enterSyncRunningState("sync", "src/a/b.md", 2, 3, { phase: "import", current_file: "src/a/b.md", files_done: 2, files_total: 3, current_chunk: 0, chunks_total: 0, }); show("import-prebatch"); // 3. the prelude (null phase, no file yet): enterSyncRunningState("sync", null, 0, 0, { phase: null, current_file: null, files_done: 0, files_total: 0, current_chunk: 0, chunks_total: 0, }); show("prelude"); // 4. the 202/409 click (no status yet): enterSyncRunningState("sync", null, 0, 0); show("click"); // 5. the upload run (no status — its endpoint has no chunk fields, D2): enterSyncRunningState("upload", null, 0, 0); show("upload"); // 6. overview (the chunk fields are cleared at this transition): enterSyncRunningState("sync", null, 4, 4, { phase: "overview", current_file: null, files_done: 4, files_total: 4, current_chunk: 0, chunks_total: 0, }); show("overview"); // 7. summaries (the phase's own (n/m) is its position): enterSyncRunningState("sync", null, 4, 4, { phase: "summaries", current_summary: "src/alpha", files_done: 4, files_total: 4, summaries_done: 2, summaries_total: 3, current_chunk: 0, chunks_total: 0, }); show("summaries"); // 8. the idle settle goes through the REAL settle (phase 134 task // 02: it hides the Cancel control too) — the settle itself goes // through the writer with no chunk text: settleSyncButton("Sync sources"); show("idle"); // 9. Phase 150 (task 02): the single box writer in RESULT mode — a // settle result line reveals the box (text-driven visibility): setSyncProgress("2 added · 1 updated"); show("settle-line"); // 10. the true idle — the writer empties the box, it hides: setSyncProgress(""); show("true-idle"); """ def _run_node(script: str) -> str: proc = subprocess.run(["node", "-e", script], capture_output=True, text=True, timeout=60) assert proc.returncode == 0, f"node failed: {proc.stderr}" return proc.stdout def _chunk_harness_rows() -> list: """The node harness's rows — the REAL extracted label path against stub element refs (the test-document-viewer pattern), one JSON row per state: [tag, title, announcer (the box's text — the sole live region, phase 150), main, count, countHidden, chunk, chunkHidden, cancelHidden, cancelDisabled, progressText, progressHidden, btnRunning, iconSpinning, ariaLabel]. Shared by the chunk-part test (phase 129) and the detail-box test (phase 149 + 150's result mode).""" js = _js() fns = "\n".join( _fn(js, name) for name in ( "fmtSyncLabel", "fmtSyncPhaseLabel", "setSyncLabel", # Phase 150 (task 02): the single box writer — the REAL # setSyncLabel calls it (running mode) and the tail's # steps 9-10 drive it in result mode. "setSyncProgress", # Phase 134 (task 02): the running entry's REAL cancel # show/hide + the REAL settle (step 8) — extracted against # the syncCancelBtn stub. "showSyncCancelBtn", "hideSyncCancelBtn", "enterSyncRunningState", "settleSyncButton", ) ) script = _CHUNK_NODE_HEAD + fns + _CHUNK_NODE_TAIL return [json.loads(line) for line in _run_node(script).splitlines() if line] @pytest.mark.skipif(not HAVE_NODE, reason="node not available") def test_chunk_part_renders_only_in_the_import_phase_under_node() -> None: """Behavioral check (node) of the REAL label path — the extracted fmtSyncLabel / fmtSyncPhaseLabel / setSyncLabel / enterSyncRunningState run against stub element refs (the test-document-viewer pattern). The pinned "· chunk n/m" part (the task's fixture values): * import, batch in flight (chunks_total 38 > 0): the chunk part shows "· chunk 14/38" and the UNTRUNCATED title + #sync-progress (the sole live region) read exactly "Syncing… src/a/b.md (2/3) · chunk 14/38" (A4); * import before the first batch (0/0): the chunk part hides and the label stays "Syncing… src/a/b.md (2/3)"; * every OTHER state — prelude (null phase), the 202/409 click (no status), the upload run (no status, D2), overview, summaries (its own (n/m) is its position), the idle settle — leaves the chunk part hidden + empty. Phase 134 (task 02): every row also snapshots the Cancel control's hidden/disabled pair — the behavioral contract: shown + enabled in EVERY sync-running state, hidden + disabled for the upload run (the upload job is NOT cancellable in this phase) and after the (real) settle. """ rows = _chunk_harness_rows() by_tag = {row[0]: row for row in rows} # row = [tag, title, announcer (the box's text — the sole live # region, phase 150), main, count, countHidden, chunk, # chunkHidden, cancelHidden, cancelDisabled, progressText, # progressHidden, btnRunning, iconSpinning, ariaLabel] # 1. the in-flight import — the chunk part renders, the full # untruncated label rides the title + the announcer (A4). row = by_tag["import-inflight"] assert row[3] == "Syncing… src/a/b.md", row assert row[4] == "(2/3)" and row[5] is False, row assert row[6] == "· chunk 14/38" and row[7] is False, ( "the chunk part shows the batch position (1-based first chunk / file total)" ) assert row[1] == "Syncing… src/a/b.md (2/3) · chunk 14/38", row[1] assert row[2] == row[1], "the announcer hears the whole thing (A4)" # 2. the import before the first batch (0/0) — the part hides, the # label is the byte-identical phase-64 string. row = by_tag["import-prebatch"] assert row[1] == "Syncing… src/a/b.md (2/3)", row[1] assert row[2] == row[1] assert row[6] == "" and row[7] is True, row # 3. every other RUNNING state — the chunk part is hidden + # empty, the title is the byte-identical pre-phase-129 label, # and the announcer (the box) hears the whole label (A4). for tag, expected_title in ( ("prelude", "Syncing…"), ("click", "Syncing…"), ("upload", "Importing"), ("overview", "Writing KB overview…"), ("summaries", "Summarizing folders… src/alpha (2/3)"), ): row = by_tag[tag] assert row[1] == expected_title, (tag, row) assert row[2] == row[1], (tag, row) assert row[6] == "" and row[7] is True, f"{tag} must hide the chunk part" # 4. the idle settle (phase 150): the settle label rides the # BUTTON only — the non-running writer call leaves the box # alone (A2: the idle/settle labels must not fill the panel), # so the announcer (the box) keeps the last running label. row = by_tag["idle"] assert row[1] == "Sync sources", row assert row[2] == "Summarizing folders… src/alpha (2/3)", ( "the idle settle leaves the box alone (the settle label stays on" " the button — phase 150, A2)" ) assert row[6] == "" and row[7] is True, "idle must hide the chunk part" # Phase 134 (task 02): the Cancel control's behavioral contract — # shown + enabled in EVERY sync-running state (the entry shows it # for kind "sync"), hidden + disabled for the upload run (the # upload job is NOT cancellable in this phase) and hidden + # disabled after the real settle (settleSyncButton always hides # it — every terminal + idle passes through it). for tag in ("import-inflight", "import-prebatch", "prelude", "click", "overview", "summaries"): row = by_tag[tag] assert row[8] is False and row[9] is False, ( f"{tag}: the Cancel control must be shown + enabled while a sync run is in flight" ) for tag in ("upload", "idle"): row = by_tag[tag] assert row[8] is True and row[9] is True, ( f"{tag}: the Cancel control must be hidden + disabled " "(the upload job is not cancellable / the settle hides it)" ) @pytest.mark.skipif(not HAVE_NODE, reason="node not available") def test_image_part_renders_only_in_the_import_phase_under_node() -> None: """Phase 152 (task 03): behavioral check (node) of the REAL label path — the extracted fmtSyncPhaseLabel / setSyncLabel / enterSyncRunningState run against the stub element refs. The pinned "· image n/m" part (the task's fixture values): * import, a PDF's vision span in flight (images_total 15 > 0) WITH the last chunk position still on the status (125/126): the composed full string carries BOTH parts — the box, the button title, and the running aria-label all read exactly "Syncing… src/a/b.md (2/3) · chunk 125/126 · image 3/15" (the owner's stuck label, now moving); the chunk part KEEPS its span ("· chunk 125/126" visible) and the image part gets NO span of its own (it rides the composed string only — the running button is the spinner, phase 149); * import, a purely pictorial PDF (the chunk position never left 0/0) in its vision span: the image part shows ALONE — "Syncing… src/a/b.md (2/3) · image 7/7" — and the chunk part stays hidden + empty (the two parts are distinct and each disappears independently on its own 0/0); * every OTHER running state (the pre-batch import, the prelude, the 202/409 click, the upload run, the overview, the summaries — the status's image fields are 0/0 or cleared at those transitions) leaves the composed string image-free. The box / Cancel-control / is-running contracts of the running state apply to the new rows exactly as to every other running state (the harness's other pins cover them per tag).""" rows = _chunk_harness_rows() by_tag = {row[0]: row for row in rows} # row = [tag, title, announcer (the box's text — the sole live # region, phase 150), main, count, countHidden, chunk, # chunkHidden, cancelHidden, cancelDisabled, progressText, # progressHidden, btnRunning, iconSpinning, ariaLabel] # 1b. both parts at once — the composed full string, byte-for-byte # (the image part LAST, after the chunk part), on the box, the # title, and the running aria-label (A4) — the chunk span # keeps its own part. full = "Syncing… src/a/b.md (2/3) · chunk 125/126 · image 3/15" row = by_tag["import-image-span"] assert row[1] == full, row[1] assert row[2] == full, "the announcer (the box) hears the whole thing (A4)" assert row[14] == full, "the running button's accessible name is the full label" assert row[10] == full, "the detail box carries the EXACT full label" assert row[6] == "· chunk 125/126" and row[7] is False, ( "the chunk part keeps its own span alongside the image part" ) # 1c. the image part alone (the chunk position never left 0/0 — # the two parts are independent). row = by_tag["import-image-only"] assert row[1] == "Syncing… src/a/b.md (2/3) · image 7/7", row[1] assert row[2] == row[1] and row[14] == row[1], row assert row[6] == "" and row[7] is True, ( "the chunk part stays hidden + empty (its own 0/0)" ) # The pre-batch import (0/0 on BOTH positions) and every other # running state: the composed string is image-free (the part # hides on its own 0/0 — the overview/summaries transitions # clear the position server-side, the bare builders carry none). for tag, expected_title in ( ("import-inflight", "Syncing… src/a/b.md (2/3) · chunk 14/38"), ("import-prebatch", "Syncing… src/a/b.md (2/3)"), ("prelude", "Syncing…"), ("click", "Syncing…"), ("upload", "Importing"), ("overview", "Writing KB overview…"), ("summaries", "Summarizing folders… src/alpha (2/3)"), ): row = by_tag[tag] assert row[1] == expected_title, (tag, row) assert "· image" not in row[1], ( f"{tag}: the image part shows only while images_total > 0" ) # The running-state contracts apply to the new rows too: box # visible + is-running + spinning, the Cancel control shown + # enabled (a sync run in flight). for tag in ("import-image-span", "import-image-only"): row = by_tag[tag] assert row[11] is False, f"{tag}: the box must be visible while in flight" assert row[12] is True and row[13] is True, f"{tag}: is-running + spinning" assert row[8] is False and row[9] is False, ( f"{tag}: the Cancel control is shown + enabled while a sync runs" ) @pytest.mark.skipif(not HAVE_NODE, reason="node not available") def test_progress_box_and_running_button_contract_under_node() -> None: """Phase 149 (task 01) — the detail box + the icon-only running button, behaviorally pinned through the SAME node harness (the REAL setSyncLabel / enterSyncRunningState / settleSyncButton / setSyncProgress against the stub element refs). The contract — while a run is in flight the box is visible and its text is EXACTLY the full untruncated label (phase 150: the box IS the announcer — one surface for sighted users and AT), and its visibility is TEXT-DRIVEN (the single writer setSyncProgress hides it iff the text is empty): * EVERY running state (the in-flight import, the pre-batch import, the prelude, the 202/409 click, the upload run, the overview, the summaries): the box is REVEALED (hidden=false) and carries the full label (the same string as the button title / announcer — the writer's `full`), the button carries `is-running` + the icon `is-spinning` (the class travels with the spin), and the button's accessible name (aria-label) is the FULL live label (the icon-only running button keeps a name); * the settle (the REAL settleSyncButton through the writer with the default `running = false`): phase 150 — the box is LEFT ALONE (the settle label rides the button only, A2; the settle paths own the box's result line through setSyncProgress, and this bare settle writes none — the box keeps the last running label, visible), `is-running` + `is-spinning` are both gone, and the aria-label is restored to the ship value "Sync sources"; * the writer's RESULT mode (phase 150): a non-empty result line reveals the box (text-driven visibility) without touching the button, and "" empties + hides the box again (the true idle). """ rows = _chunk_harness_rows() by_tag = {row[0]: row for row in rows} # row = [tag, title, announcer (the box's text — the sole live # region, phase 150), main, count, countHidden, chunk, # chunkHidden, cancelHidden, cancelDisabled, progressText, # progressHidden, btnRunning, iconSpinning, ariaLabel] # Every running state: box visible + the EXACT full label + # is-running + spinning + the full-label accessible name. for tag in ( "import-inflight", "import-prebatch", "prelude", "click", "upload", "overview", "summaries", ): row = by_tag[tag] assert row[11] is False, ( f"{tag}: the detail box must be VISIBLE while the run is in flight" ) assert row[10] == row[1], ( f"{tag}: the box carries the EXACT full label (title == box)" ) assert row[12] is True, ( f"{tag}: the button must carry is-running while in flight" ) assert row[13] is True, ( f"{tag}: is-running travels with the spin (the icon spins)" ) assert row[14] == row[1], ( f"{tag}: the running button's accessible name is the full live label" ) # The settle (phase 150): the box is LEFT ALONE — the settle # label rides the button only (A2), so the box keeps the last # running label, still visible; the running state is fully # cleared, and the accessible name is back to the ship value. row = by_tag["idle"] assert row[1] == "Sync sources", "the settle label rides the button" assert row[11] is False, ( "the bare settle leaves the box alone (phase 150: visibility is" " text-driven via setSyncProgress — the settle writes no box text)" ) assert row[10] == "Summarizing folders… src/alpha (2/3)", ( "the box keeps the last running label (the settle label stays on" " the button — A2)" ) assert row[12] is False, "is-running is removed on the settle (with the spin)" assert row[13] is False, "the spin is removed on the settle" assert row[14] == "Sync sources", ( "the settle restores the ship aria-label (the idle truth)" ) # The writer's result mode (phase 150): a result line reveals the # box (text-driven visibility) without touching the button; "" # empties + hides the box (the true idle). row = by_tag["settle-line"] assert row[1] == "Sync sources", ( "setSyncProgress owns the box only — the button title is untouched" ) assert row[10] == "2 added · 1 updated", row assert row[11] is False, "the settle result line reveals the box" row = by_tag["true-idle"] assert row[10] == "", "the true idle empties the box" assert row[11] is True, "the true idle hides the box (text-driven)" def test_every_other_state_passes_no_chunk_text() -> None: """Source-level pin of the render-only-while-chunks_total>0 + render-only-while-images_total>0 contracts: the import fall-through is the ONLY place in sources.js that reads the status's chunk OR image fields (the summaries/overview branches and the bare builders never touch them), and every NON-running label emission passes no chunk/image text (both parts hide): the settle/idle labels go through setSyncLabel(label) — the only running-flagged call site (phase 149: `running = true`) is the running entry (phase 152: passing the image part as its fifth argument).""" js = _js() body = _fn(js, "fmtSyncPhaseLabel") i_fall = body.find("fmtSyncLabel(") pre = body[:i_fall] assert "chunks_total" not in pre and "current_chunk" not in pre, ( "the overview/summaries branches never read the chunk fields" ) assert "images_total" not in pre and "images_done" not in pre, ( "the overview/summaries branches never read the image fields (phase 152)" ) fall = body[i_fall:] assert "status.chunks_total" in fall and "status.current_chunk" in fall assert "status.images_total" in fall and "status.images_done" in fall, ( "the import fall-through is the ONLY reader of the image fields (phase 152)" ) assert js.count("setSyncLabel(main, count, chunk, true, image)") == 1, ( "the ONLY running-flagged call site is the running entry (phase 149" " — phase 152: with the image part)" ) assert "setSyncLabel(label)" in js, ( "the settle/idle labels stay two-arg (no chunk/image)" ) # ---------- the two-job tick (startSyncPolling) ---------- def test_tick_fetches_both_jobs_with_403_and_blip_rules() -> None: """Each tick fetches BOTH status endpoints (the sync and the background upload run — phase 90: unpack + register, no scan). The 403 backstop (button hidden) applies to the SYNC fetch only — a 403 on the upload fetch is simply "no upload" (never a hide); a network blip on either fetch retries next tick (the tick reschedules, it never dies on a failed fetch).""" tick = _tick(_js()) assert 'fetch("/api/sync/status")' in tick assert 'fetch("/api/git-sources/upload/status")' in tick assert "r.status === 403" in tick, "the sync fetch keeps the whoami backstop" assert "ur.status === 403" not in tick, "upload 403 = no upload (A3)" assert "ur.ok" in tick, "the upload fetch is read only when ok" assert tick.count("catch {") >= 2, "both fetches are blip-tolerant" assert tick.count("setTimeout(tick, SYNC_POLL_MS)") >= 3, ( "the blip and both running branches all reschedule" ) # the sync-status blip must not settle the button: it reschedules. i_nostatus = tick.find("if (!syncStatus)") assert i_nostatus != -1 branch = tick[i_nostatus : i_nostatus + 200] assert "setTimeout(tick, SYNC_POLL_MS)" in branch assert "applySyncIdle" not in branch, "a blip is not an idle" def test_tick_decision_tree_order_and_branches() -> None: """The phase-64 decision tree, in order (A3/A4): 1. sync running → 2. upload running → 3. sync success → 4. sync failed → 5. upload success → 6. upload failed → 7. both idle. The running branches enter the running state with THEIR job's kind + live file/counts; the sync terminals are the unchanged phase-32 appliers; the upload terminals settle "Sync sources" + clear the box (setSyncProgress("") — phase 150: the box hides, empty) + hide a stale error + emit a synthetic idle frame — never the upload's status object, never fmtSyncResult (no upload counts in the box, A3); only the upload SUCCESS refreshes the catalog (the KB changed); the upload failure raises no error surface on this page (the banner is the Sources page's).""" tick = _tick(_js()) b_sync_run = tick.find('syncStatus.state === "running"') b_up_run = tick.find('uploadStatus && uploadStatus.state === "running"') b_sync_ok = tick.find('syncStatus.state === "success"') b_sync_fail = tick.find('syncStatus.state === "failed"') b_up_ok = tick.find('uploadStatus && uploadStatus.state === "success"') b_up_fail = tick.find('uploadStatus && uploadStatus.state === "failed"') b_idle = tick.find("applySyncIdle(syncStatus)") assert ( -1 < b_sync_run < b_up_run < b_sync_ok < b_sync_fail < b_up_ok < b_up_fail < b_idle ), "the tree must fire in the documented order" # 1. sync running: the sync-kind live label, reschedule. branch1 = tick[b_sync_run:b_up_run] assert ( '"sync", syncStatus.current_file, syncStatus.files_done, syncStatus.files_total' in branch1 ) assert "setTimeout(tick, SYNC_POLL_MS)" in branch1 # 2. upload running: the upload-kind live label (A3), reschedule. branch2 = tick[b_up_run:b_sync_ok] assert ( '"upload", uploadStatus.current_file, uploadStatus.files_done, uploadStatus.files_total' in branch2 ) assert "setTimeout(tick, SYNC_POLL_MS)" in branch2 # 3 + 4. the sync terminals are the unchanged phase-32 appliers. assert "applySyncSuccess(syncStatus)" in tick[b_sync_ok:b_sync_fail] assert "applySyncFailure(syncStatus)" in tick[b_sync_fail:b_up_ok] # 5. upload success: settle + clear + hide stale error + emit idle # + the catalog refresh (the new documents must appear). up_ok = tick[b_up_ok:b_up_fail] for line in ( 'settleSyncButton("Sync sources")', 'setSyncProgress("")', # phase 150: the box hides, empty (A3) "hideSyncError()", 'emitSyncStatus({ state: "idle" })', "loadTree()", # phase 97: the catalog load is the tree fetch ): assert line in up_ok, f"the upload-success settle must carry {line!r}" assert "fmtSyncResult" not in up_ok, "no upload counts in the box (A3)" assert "applySyncSuccess" not in up_ok, "the sync applier is never an upload branch" # 6. upload failed: settle only — no catalog refresh (the KB did # not change) and no error surface on this page (A3). up_fail = tick[b_up_fail:b_idle] for line in ( 'settleSyncButton("Sync sources")', 'setSyncProgress("")', # phase 150: the box hides, empty (A3) "hideSyncError()", 'emitSyncStatus({ state: "idle" })', ): assert line in up_fail, f"the upload-failed settle must carry {line!r}" assert "loadTree()" not in up_fail, "no KB change on a failed upload" assert "showSyncError" not in up_fail, "no banner on this page (A3)" assert "applySyncFailure" not in up_fail and "showSyncModal" not in up_fail # 7. both idle: the unchanged idle settle. assert "stopSyncPolling()" in tick[b_idle - 60 : b_idle + 40] # ---------- the click branch + the load-time re-attach ---------- def test_click_branch_enters_sync_running_without_a_file() -> None: """The 202/409 branch of startSync enters the running state with the sync kind and no file yet (the run is just starting — model check / clone-pull: bare "Syncing…", A4); the emitSyncStatus({ state: "running" }) dedup via lastSyncState stays, and the poll starts.""" js = _js() idx = js.find("r.status === 202 || r.status === 409") assert idx != -1 branch = js[idx : idx + 500] assert "enterSyncRunningState(\"sync\", null, 0, 0)" in branch assert 'emitSyncStatus({ state: "running" })' in branch assert "lastSyncState !== \"running\"" in branch, "the dedup stays" assert "startSyncPolling()" in branch def test_reattach_adopts_a_running_upload_only() -> None: """initSyncButton: the sync terminals render the last result; the RUNNING sync re-enters with the live file AND the whole status (phase 98 task 02: the label is phase-aware — a mid-summaries reload re-enters with the summaries label). With the sync IDLE it fetches the upload status: a RUNNING upload run re-attaches (running state, upload kind — phase 90: no live file, the label stays bare "Importing…" — the synthetic running frame, the poll starts); a terminal upload is a no-op — the fall-through is the plain idle settle (the boot-time loadTree() already shows the current catalog).""" body = _fn(_js(), "initSyncButton") assert "await fetchIsAdmin()" in body, "admin-only (no extra fetch)" assert 'fetch("/api/git-sources/upload/status")' in body # the sync running re-attach now carries the live file too assert '"sync", status.current_file, status.files_done, status.files_total' in body # the ONLY upload branch is the running one (A3) assert "upload && upload.state === \"running\"" in body assert 'upload.state === "success"' not in body, "a terminal upload never re-attaches" assert 'upload.state === "failed"' not in body, "a terminal upload never re-attaches" i_check = body.find("upload && upload.state === \"running\"") i_fall = body.find("applySyncIdle(status)", i_check) assert -1 < i_check < i_fall branch = body[i_check:i_fall] assert '"upload", upload.current_file, upload.files_done, upload.files_total' in branch assert 'emitSyncStatus({ state: "running" })' in branch assert "startSyncPolling()" in branch # the idle settle is the fall-through (the last statement — the # brace-balanced body ends with the closing brace) assert body.rstrip().removesuffix("}").rstrip().endswith("applySyncIdle(status);") def test_tick_sync_running_passes_the_status_for_the_phase_label() -> None: """Branch 1 of the two-job tick (phase 98 task 02): the sync running state passes the WHOLE syncStatus object as the fifth argument — the phase-aware label (overview / summaries) reads its phase fields. The upload running branch stays on the BARE four-arg label (its status has no phase — D2): no fifth argument, no status object anywhere in the branch.""" tick = _tick(_js()) b_sync_run = tick.find('syncStatus.state === "running"') b_up_run = tick.find('uploadStatus && uploadStatus.state === "running"') b_sync_ok = tick.find('syncStatus.state === "success"') assert -1 < b_sync_run < b_up_run < b_sync_ok branch1 = tick[b_sync_run:b_up_run] i_call = branch1.find("enterSyncRunningState(") call = branch1[i_call:branch1.find(");", i_call)] assert re.search( r'"sync",\s*syncStatus\.current_file,\s*syncStatus\.files_done,\s*' r"syncStatus\.files_total,\s*syncStatus\s*$", call, ), "branch 1 passes the whole status (fifth arg) for the phase-aware label" branch2 = tick[b_up_run:b_sync_ok] i_call2 = branch2.find("enterSyncRunningState(") call2 = branch2[i_call2:branch2.find(");", i_call2)] assert re.fullmatch( r'enterSyncRunningState\(\s*"upload",\s*uploadStatus\.current_file,\s*' r"uploadStatus\.files_done,\s*uploadStatus\.files_total\s*$", call2, ), "the upload job keeps the bare four-arg label — no status, no phase (D2)" assert "phase" not in call2 and "summaries" not in call2 def test_reattach_sync_running_passes_the_status_for_the_phase_label() -> None: """initSyncButton (phase 98 task 02): the RUNNING sync re-attach passes the WHOLE status object — a reload mid-summaries re-enters the running state with the summaries label (the never-stale contract); the RUNNING upload re-attach stays on the bare four-arg label (its status has no phase — D2).""" body = _fn(_js(), "initSyncButton") i_sync = body.find('"sync", status.current_file') assert i_sync != -1 call = body[i_sync:body.find(");", i_sync)] assert re.search( r'"sync",\s*status\.current_file,\s*status\.files_done,\s*' r"status\.files_total,\s*status\s*$", call, ), "the RUNNING sync re-attach passes the status object (fifth arg)" i_up = body.find('"upload", upload.current_file') assert i_up != -1 call2 = body[i_up:body.find(");", i_up)] assert re.fullmatch( r'"upload",\s*upload\.current_file,\s*upload\.files_done,\s*upload\.files_total\s*$', call2, ), "the RUNNING upload re-attach keeps the bare four-arg label (D2)" # ---------- the section header + the page comment ---------- def test_section_header_documents_the_two_job_contract() -> None: """The sync-button section marker comment documents the phase-64 contract: the live file label, the two-job decision tree (both status endpoints), and the A3 settle behavior (catalog refresh; the upload counts never render here).""" js = _js() marker = js.find("Sync sources button (Sources page only)") assert marker != -1, "the sync section marker comment must stay" header = js[marker : js.find("const syncBtn")] assert "Phase 64 (task 04)" in header assert "/api/git-sources/upload/status" in header, "the second job's endpoint" assert "loadTree" in header, "the A3 catalog refresh (phase 97 rename)" assert "A3" in header and "A4" in header def test_sources_html_comment_documents_the_live_announcer() -> None: """The #sync-progress comment in the shell's RAG view (formerly sources.html) documents the box's role as the SOLE aria-live announcer (phase 150): it carries the full untruncated live file label while a sync run is in flight AND the settle result line after it ("N added · …", "Sync cancelled", the cancel-failure copy), hiding only when its text is empty — one live region, no double announce (the retired announcer span is gone).""" html = _html() idx = html.find('id="sync-progress"') assert idx != -1 comment = html[max(0, idx - 1200):idx] assert "SOLE aria-live region" in comment, ("the single-live-region contract is documented") assert "no double" in comment, "the no-double-announce contract is documented" assert "Syncing…" in comment, "the sync live-file label is documented" assert "Sync cancelled" in comment, "the settle result line is documented" assert "Phase 150" in comment, "the announcer fold-in is documented" # ---------- styles.css: the ellipsized live label ---------- def test_sync_label_css_ellipsis_truncation() -> None: """.sync-label (phase 128 task 02): the live-file label is a FLEX ROW of two parts inside the pill (A4) — the container is inline-flex with the min(16rem, 40vw) cap + min-width: 0; the .sync-label-main part is the shrinkable item (min-width: 0, overflow hidden, text-overflow ellipsis, no wrap) whose long source/relative/path ellipsizes; the .sync-label-count part is flex 0 0 auto — NEVER clipped — in the .sync-progress-text mono language (a position reads as data). On mobile the pill goes full width and the label FILLS it (the cap lifts, flex 1 1 auto) so the main part ellipsizes against the width left by the pinned count (the icon-only squeeze is gone).""" css = _css() block = re.search(r"\.sync-label\s*\{([^}]*)\}", css) assert block, "styles.css must style .sync-label" body = block.group(1) for prop in ( "display: inline-flex", "align-items: baseline", "gap: 0.35rem", "min-width: 0", "max-width: min(16rem, 40vw)", ): assert prop in body, f".sync-label must carry {prop!r}" assert "inline-block" not in body, "the old inline-block container is gone" assert "vertical-align" not in body, "the flex row aligns the parts" main_block = re.search(r"\.sync-label-main\s*\{([^}]*)\}", css) assert main_block, "styles.css must style .sync-label-main" for prop in ( "min-width: 0", "overflow: hidden", "text-overflow: ellipsis", "white-space: nowrap", ): assert prop in main_block.group(1), f".sync-label-main must carry {prop!r}" count_block = re.search(r"\.sync-label-count\s*\{([^}]*)\}", css) assert count_block, "styles.css must style .sync-label-count" for prop in ( "flex: 0 0 auto", "white-space: nowrap", "font-family: var(--mono)", ): assert prop in count_block.group(1), f".sync-label-count must carry {prop!r}" mobile = re.search(r"@media \(max-width: 640px\) \{([\s\S]*?)\n\}", css) assert mobile, "the ≤640px media query must stay" mbody = mobile.group(1) assert ".sync-btn { width: 100%; }" in mbody, ( "the mobile Sync pill is full width (the icon-only squeeze is gone)" ) assert ".sync-label { max-width: none; flex: 1 1 auto; min-width: 0; }" in mbody, ( "the label fills the full-width pill; the main part ellipsizes against the pinned count" ) # ===================================================================== # Phase 64 task 05 — the Sources-page upload (git-sources.js) # ===================================================================== # ---------- the "Successfully uploaded" toast (A2) ---------- def test_upload_toast_single_node_status_and_auto_dismiss() -> None: """showUploadToast (A2 owner-locked): the phase-55 share-toast pattern made page-local — a SINGLE node, lazy-created on the first 202 and reused (toasts never stack): a plain ``
`` appended to ``document.body``; ``role="status" aria-live="polite"`` (on THIS page the toast is the a11y announcer for the 202); the text lands via ``textContent`` (XSS-safe — never innerHTML). A new toast replaces a pending one: clear the prior dismiss timer, remove the visible class, force a reflow (``offsetWidth`` — restarts the CSS transition), re-add the class. Auto-dismiss: the 5000ms (UPLOAD_TOAST_MS) timer is armed AFTER the visible class is added and removes the class on fire. The existing .toast CSS is reused as-is (no new styles).""" js = _gjs() body = _gfn(js, "showUploadToast") assert "if (!uploadToastEl)" in body, "the node is created once, on first use" assert 'document.createElement("div")' in body assert 'uploadToastEl.className = "toast"' in body, "the phase-55 .toast CSS, as-is" assert 'uploadToastEl.setAttribute("role", "status")' in body assert 'uploadToastEl.setAttribute("aria-live", "polite")' in body assert "document.body.appendChild(uploadToastEl)" in body assert "uploadToastEl.textContent = message" in body assert "innerHTML" not in body, "XSS contract: textContent only" # Single instance: module-scope node + timer. assert re.search(r"^let uploadToastEl = null", js, re.M), "the node is module scope" assert re.search(r"^let uploadToastTimer = 0", js, re.M), "the timer is module scope" assert "const UPLOAD_TOAST_MS = 5000" in js, "the ~5 s auto-dismiss (A2)" # Re-trigger order: clear dismiss → remove class → force reflow → # re-add the visible class. clear_i = body.find("clearTimeout(uploadToastTimer)") remove_i = body.find('uploadToastEl.classList.remove("is-visible")') reflow_i = body.find("void uploadToastEl.offsetWidth") add_i = body.find('uploadToastEl.classList.add("is-visible")') assert -1 < clear_i < remove_i < reflow_i < add_i, ( "dismiss cleared → class removed → reflow forced → visible re-added" ) # The auto-dismiss timer is armed AFTER the visible class is set. timer_i = body.find("setTimeout") assert -1 < add_i < timer_i and "UPLOAD_TOAST_MS" in body[timer_i:] assert 'uploadToastEl.classList.remove("is-visible")' in body[timer_i:], ( "the pending dismiss removes the visible state" ) def test_toast_fires_on_202_with_the_safe_name() -> None: """The 202 branch of the upload submit (A1/A2): the 202 body (UploadAccepted) is parsed for the safe source name — a body parse failure degrades to the picked file's name — and the toast fires with `Successfully uploaded — ` BEFORE the scan finishes: the file input clears, the processing state enters, and the poll starts. The toast is the SINGLE success surface: exactly one call site in the whole page (definition + one call — never a failure branch, never the poll).""" js = _gjs() sub = _usubmit(js) i202 = sub.find("r.status === 202") i409 = sub.find("r.status === 409") assert -1 < i202 < i409, "the 202 branch precedes the 409 re-attach" branch = sub[i202:i409] assert "let name = file.name;" in branch, "the degrade-to-picked-name fallback" assert "await r.json()" in branch, "the UploadAccepted body is parsed" assert "data.name" in branch, "the safe source name comes from the 202 body" assert "lastUploadName = name" in branch, ( "the accepted 202's safe name is recorded for the result line (phase 90)" ) assert "showUploadToast(`Successfully uploaded — ${name}`)" in branch assert 'uploadFileInput.value = ""' in branch, "the file input clears at 202" assert "enterUploadProcessingState()" in branch assert "startUploadPolling()" in branch # Success-only: exactly the definition + the single 202 call. assert js.count("showUploadToast(") == 2, ( "the definition + exactly ONE call site (the 202 branch)" ) # ---------- the processing state + the live label ---------- def test_processing_state_is_bare_for_the_whole_run() -> None: """Phase 90 (A2): the button's processing entry (202 / 409) is disabled, BARE "Processing…", title cleared — and the tick's RUNNING branch renders exactly that same bare label for the whole background run: the unpack has no file-level progress, so the phase-64 live-file interpolation is gone (no current_file, no "(n/m)" counts, no file in the title), then it reschedules at the 2 s house cadence.""" js = _gjs() state = _gfn(js, "enterUploadProcessingState") assert "uploadBtn.disabled = true" in state assert 'uploadBtn.textContent = "Processing…"' in state assert 'uploadBtn.title = "";' in state, "the title stays clear (no live file)" assert "const UPLOAD_POLL_MS = 2000" in js, "the SYNC_POLL_MS house value" tick = _utick(js) i_run = tick.find('status.state === "running"') i_stop = tick.find("stopUploadPolling();") assert -1 < i_run < i_stop, "the running branch precedes the terminal stop" run = tick[i_run:i_stop] assert 'uploadBtn.textContent = "Processing…"' in run, ( "the bare label — no file, no counts (phase 90, A2)" ) assert 'uploadBtn.title = "";' in run, "the title stays clear" assert "status.current_file" not in run, ( "no live file — the scan's progress moved to the sync" ) assert "files_total" not in run and "files_done" not in run, "no (n/m) counts" assert "uploadPollTimer = setTimeout(tick, UPLOAD_POLL_MS)" in run, "reschedule" def test_fmt_upload_result_is_the_ready_for_sync_line() -> None: """Phase 90 (A2/A3) extended by Phase 154 (task 02): the result line reads the no-count {"message": "uploaded"} detail and renders the ready-for-sync line in the FOUR phase-154 shapes — "Uploaded () — press Sync sources to import it." (the name + the completion stamp), "Uploaded — press Sync sources to import it." (name, no stamp — BYTE-IDENTICAL to the pre-phase string), "Uploaded — press Sync sources to import it." (no name + stamp — the boot re-render, self-dating with the name unknown), "Uploaded — press Sync sources to import it." (no name, no stamp — BYTE-IDENTICAL to the pre-phase string; the running / legacy / partial payload). The non-uploaded detail keeps the unchanged "The upload finished."; the old sync-style count keys (added / updated / unchanged / pruned) are no longer read at all. The 202 branch records the safe name (lastUploadName); the poll's settle and the boot re-attach both render the line from (detail, lastUploadName, fmtUploadStamp(status)); the button's idle label is "Upload".""" js = _gjs() body = _gfn(js, "fmtUploadResult") assert "function fmtUploadResult(detail, name, when)" in body, ( "phase 154: the precomputed completion stamp is the third parameter" ) assert 'detail.message === "uploaded"' in body # The four shapes (name×stamp, name×no-stamp, no-name×stamp, # no-name×no-stamp). assert "Uploaded ${name} (${when}) — press Sync sources to import it." in body assert "Uploaded ${name} — press Sync sources to import it." in body, ( "the no-stamp nameful variant — byte-identical to pre-phase" ) assert "Uploaded ${when} — press Sync sources to import it." in body, ( "the nameless + stamp variant (the boot re-render — self-dating)" ) assert "Uploaded — press Sync sources to import it." in body, ( "the nameless no-stamp variant — byte-identical to pre-phase" ) assert '"The upload finished."' in body, ("the non-uploaded branch is unchanged") for key in ("added", "updated", "unchanged", "pruned"): assert key not in body, f"the sync-style count {key!r} is gone" # The 202 branch records the safe name for the settled line. sub = _usubmit(js) i202 = sub.find("r.status === 202") i409 = sub.find("r.status === 409") assert "lastUploadName = name" in sub[i202:i409] assert "let lastUploadName = null" in js, "page-local (null after a reload)" # Both render sites pass (…, lastUploadName, fmtUploadStamp(status)). tick = _utick(js) assert re.search( r"uploadResult\.textContent = fmtUploadResult\(\s*detail,\s*" r"lastUploadName,\s*fmtUploadStamp\(status\)\s*\)", tick, ), "the poll's success settle passes the completion stamp (phase 154)" init = _gfn(js, "initUploadStatus") assert re.search( r"uploadResult\.textContent = fmtUploadResult\(\s*status\.detail,\s*" r"lastUploadName,\s*fmtUploadStamp\(status\)\s*\)", init, ), "the boot re-attach passes the completion stamp (phase 154)" # The button's idle label (static + the poll's restores). restore = _gfn(js, "restoreUploadButton") assert 'uploadBtn.textContent = "Upload"' in restore # The phase-64 scan-suffixed label is gone (exactly "Upload") — the # needles are split so this file carries no forbidden literal (the # phase-90 rg criterion sweeps frontend/ app/ tests/ for it). assert ("Upload " + "& scan") not in js and ("Upload " + "and scan") not in js def test_start_upload_polling_double_start_guard() -> None: """startUploadPolling: single timer, one loop at a time — the first statement bails when a poll is already active (the guard a double 202/409 cannot bypass).""" js = _gjs() i = js.find("function startUploadPolling") assert i != -1 head = js[i : i + 120] assert "if (uploadPollTimer !== null) return;" in head assert "let uploadPollTimer = null" in js, "null = no poll active" # ---------- the 409 re-attach + the kept error branches ---------- def test_409_reattaches_without_an_error_banner() -> None: """409 (an upload is already in progress): NO error banner — the phase-49 "server detail inline" branch does not apply to 409 anymore. It re-attaches to the in-flight run: the processing state + the poll (never stale). The OTHER non-2xx (422 format/name, 413 cap, 5xx) keep the phase-49 apiDetail banner + the kept file selection; the network-failure fixed line stays.""" js = _gjs() sub = _usubmit(js) i409 = sub.find("r.status === 409") assert i409 != -1 branch = sub[i409 : sub.find("// Other non-2xx", i409)] assert "enterUploadProcessingState()" in branch assert "startUploadPolling()" in branch assert "uploadError" not in branch, "409 never raises the error banner" assert "apiDetail" not in branch, "no server-detail branch for 409 anymore" # The other non-2xx keeps the phase-49 convention (after the 409). i_err = sub.find('await apiDetail(r, "Could not upload the archive — try again.")') assert i_err > i409, "the other non-2xx branch follows the 409 re-attach (and was found)" assert "uploadError.hidden = false" in sub[i_err:], "the banner shows for the other non-2xx" assert "Could not upload the archive — is the app reachable?" in sub, "the network line stays" # The no-file guard + the short transfer label stay. assert "Choose an archive file to upload." in sub assert 'uploadBtn.textContent = "Uploading…"' in sub # ---------- the poll's terminal decision tree ---------- def test_upload_polling_decision_tree() -> None: """The upload poll tick (task 05): fetches GET /api/git-sources/upload/status; a blip (non-ok / network / unparseable) reschedules — the tick never dies on a failed fetch. Then: running → live label + reschedule; ONE stop for the terminals; success → the result line (fmtUploadResult — the existing helper reads exactly these keys) + the announce + the row reload + the cleared file input + the restored button — NO toast (it already fired at the 202, A2); failed → the sanitized server error banner (A2 failure UI) + the restored button + the row reload (a post-swap failure keeps the row — the list state may have changed; the selection is kept for a one-click re-upload); idle → the defensive restore (a started run never returns to idle).""" tick = _utick(_gjs()) assert 'fetch("/api/git-sources/upload/status")' in tick # The blip branch reschedules. i_blip = tick.find("if (!status)") assert i_blip != -1 assert "uploadPollTimer = setTimeout(tick, UPLOAD_POLL_MS)" in tick[i_blip:i_blip + 150] # One stop, placed after the running branch and before the terminals. assert tick.count("stopUploadPolling()") == 1 i_run = tick.find('status.state === "running"') i_stop = tick.find("stopUploadPolling();") i_ok = tick.find('status.state === "success"') i_fail = tick.find('status.state === "failed"') assert -1 < i_run < i_stop < i_ok < i_fail, "running < stop < success < failed" # success: the ready-for-sync line (phase 90 A2/A3) + the # next-step announce + reload, NO toast. ok = tick[i_ok:i_fail] for line in ( "fmtUploadResult(", "uploadResult.hidden = false", 'announce("Archive uploaded — press Sync sources to import it.")', 'uploadFileInput.value = ""', "restoreUploadButton()", "loadSources()", ): assert line in ok, f"the success settle must carry {line!r}" assert re.search( r"uploadResult\.textContent = fmtUploadResult\(\s*detail,\s*" r"lastUploadName,\s*fmtUploadStamp\(status\)\s*\)", ok, ), "the success settle passes the completion stamp (phase 154)" assert "showUploadToast" not in tick, "no toast in the poll — it fired at the 202 (A2)" # failed: the sanitized error banner + reload. fail = tick[i_fail:] assert "status.error" in fail assert "uploadError.hidden = false" in fail assert "restoreUploadButton()" in fail assert "loadSources()" in fail, "the list state may have changed" # idle: the defensive fall-through — a final restore, no more state # checks after the failed branch. i_idle = tick.rfind("restoreUploadButton()") assert i_idle > i_fail assert "state ===" not in tick[i_idle:], "the idle settle is the fall-through" # ---------- the finally's never-restore-while-polling guard ---------- def test_finally_restores_only_when_no_poll_active() -> None: """The submit finally (PLAN §7.4): the button is restored ONLY when no poll is active (``uploadPollTimer === null``) — while startUploadPolling owns the button (the 202 / 409 paths) it stays disabled / "Processing…", so an unconditional finally restore would race the poll and leave a stale-looking idle button under a running scan.""" sub = _usubmit(_gjs()) i = sub.find("} finally {") assert i != -1 fin = sub[i:] assert "if (uploadPollTimer === null)" in fin, "the guard: no poll → the button is ours" assert "restoreUploadButton()" in fin assert "uploadBtn.disabled = false" not in fin, "no unconditional restore in the finally" # ---------- the boot re-attach ---------- def test_boot_reattach_branches() -> None: """initUploadStatus (the admin branch of the boot): the upload status is fetched ONCE. running → the processing state + the poll (a reload mid-scan re-attaches — no second upload, no error, no toast); success → the last result line ONLY (no announce, no toast — A2); failed → the error banner; idle → nothing (no branch). The boot IIFE awaits it right after loadSources().""" js = _gjs() body = _gfn(js, "initUploadStatus") assert body.count('fetch("/api/git-sources/upload/status")') == 1, "fetched ONCE at boot" i_run = body.find('status.state === "running"') i_ok = body.find('status.state === "success"') i_fail = body.find('status.state === "failed"') assert -1 < i_run < i_ok < i_fail run = body[i_run:i_ok] assert "enterUploadProcessingState()" in run assert "startUploadPolling()" in run assert "uploadError" not in run and "showUploadToast" not in run ok = body[i_ok:i_fail] # The last result line — the nameless variant after a reload # (lastUploadName is null: the safe name was page-local, phase 90), # self-dating through the completion stamp (phase 154 — the stamp # comes from the server payload). assert re.search( r"uploadResult\.textContent = fmtUploadResult\(\s*status\.detail,\s*" r"lastUploadName,\s*fmtUploadStamp\(status\)\s*\)", ok, ), "the boot re-attach passes the completion stamp (phase 154)" assert "uploadResult.hidden = false" in ok assert "announce(" not in ok, "no announce at boot (A2)" assert "showUploadToast" not in ok, "no toast at boot (A2)" fail = body[i_fail:] assert "status.error" in fail assert "uploadError.hidden = false" in fail assert 'status.state === "idle"' not in body, "idle does nothing — no branch" # The mount's tail (phase 76 task 02 — the boot IIFE is gone): # after the list loads, the re-attach runs (admin branch only — the # anonymous path returns before it), and it is the LAST statement # of mount(root). i_boot = js.rfind("await loadSources();") tail = js[i_boot:i_boot + 400] assert "await initUploadStatus();" in tail assert "})();" not in js, "the top-level boot IIFE is gone (mount owns boot)" assert tail.rstrip().removesuffix("}").rstrip().endswith("await initUploadStatus();") # ---------- the page comment ---------- def test_git_sources_html_comment_documents_the_202_contract() -> None: """The #archive-upload-form comment in the shell's Sources view (formerly git-sources.html) documents the phase-64 202 contract (the phase-49 synchronous paragraph marked superseded): the 202 = "safely on disk" + the JS-created toast (no markup), the bare "Processing…" label via the status poll, the 409 re-attach without an error banner — and phase 90's unpack-only rework (the run is UNPACK + REGISTER ONLY and the success line points at the RAG page's "Sync sources" button).""" html = _ghtml() idx = html.find('id="archive-upload-form"') assert idx != -1 comment = html[max(0, idx - 1600):idx] assert "Phase 64" in comment assert "superseded" in comment, "the phase-49 synchronous paragraph is marked superseded" assert "Successfully uploaded" in comment, "the toast is documented" assert "Processing…" in comment, "the (now bare) label is documented" assert "GET /api/git-sources/upload/status" in comment, "the polling endpoint" assert "409 re-attaches" in comment, "the re-attach without an error banner" assert "Phase 90" in comment, "the unpack-only rework is documented" assert "UNPACK + REGISTER ONLY" in comment assert "Sync sources" in comment, "the result line points at the RAG page's button" # ===================================================================== # Phase 154 (task 02) — the Sources upload line's completion stamp # ===================================================================== def test_fmt_upload_stamp_is_the_duplicated_qualifier() -> None: """Phase 154 (task 02): ``fmtUploadStamp(status)`` — the page-local DUPLICATE of sources.js's ``fmtSyncTime`` date-qualifier (the per-page duplication idiom — not shared). It reads ``status.finished_at`` first (the success state always has it on the server), falls back to ``status.started_at`` (the defensive guard — a corrupt ``finished_at`` degrades to the ``started_at`` stamp rather than to no stamp), and yields ``""`` when both are missing/absent or every ISO is unparseable (the byte-identical pre-phase-154 fallback). The qualifier is the SAME A1 structure as the phase-154 task-01 pin in test_sync_button.py — local calendar dates, ``HH:MM`` same day, ``M/D, h:MM AM/PM`` same year, ``M/D/YYYY, h:MM AM/PM`` across years, the 12-hour form WITHOUT a leading zero on the hour, month/day without leading zeros — cited in sources.js, not restated. The ``status || {}`` guard defends a missing status object.""" js = _gjs() body = _gfn(js, "fmtUploadStamp") assert "function fmtUploadStamp(status)" in body assert "const s = status || {};" in body, ( "defensive: a missing / null status object yields ''" ) assert "s.finished_at" in body and "s.started_at" in body i_fin = body.find("s.finished_at") i_st = body.find("s.started_at") assert -1 < i_fin < i_st, "finished_at is read first — the started_at fallback" assert "if (!iso) continue;" in body, ( "absent / null timestamps are skipped (the fallback reads)" ) assert "if (!Number.isNaN(t.getTime()))" in body, ( "an unparseable ISO is skipped — a corrupt finished_at degrades" " to started_at, '' when neither parses" ) assert 'if (d === null) return "";' in body, ( "both absent (or unparseable) → '' — the byte-identical pre-" "phase-154 fallback" ) # The duplicated A1 qualifier (cited in sources.js's fmtSyncTime, # not restated) — the same structure as the task-01 pin. assert "const now = new Date();" in body, "'now' is the page's clock" assert ( "d.getFullYear() === now.getFullYear() &&" in body and "d.getMonth() === now.getMonth()" in body and "d.getDate() === now.getDate()" in body ), "same local calendar day as now → HH:MM (byte-identical to pre-phase)" assert "d.getHours() % 12 || 12" in body, ( "12-hour WITHOUT leading zero on the hour (the 12 o'clock edge)" ) assert '"AM"' in body and '"PM"' in body, "the AM/PM family" assert "pad(d.getMinutes())" in body, "MM zero-padded in every form" assert "`${d.getMonth() + 1}/${d.getDate()}`" in body, ( "M/D, no leading zeros (same year, different day)" ) assert "`${d.getMonth() + 1}/${d.getDate()}/${d.getFullYear()}`" in body, ( "M/D/YYYY across years" ) @pytest.mark.skipif(not HAVE_NODE, reason="node not available") def test_upload_result_four_shapes_under_node() -> None: """Phase 154 (task 02): the REAL git-sources.js ``fmtUploadResult`` under node — the four shapes, ``when`` PRECOMPUTED by the caller (so the shape itself is clock-free): * name + stamp: ``Uploaded docs.zip (21:14) — press Sync sources to import it.`` (the fresh 202 settle, the page-local name); * name, no stamp (``""``): ``Uploaded docs.zip — press Sync sources to import it.`` — BYTE-IDENTICAL to the pre-phase-154 string (the running / legacy / partial payload); * no name + stamp: ``Uploaded 10/2, 3:14 PM — press Sync sources to import it.`` (the boot re-render — self-dating with the name unknown); * no name, no stamp (``""``): ``Uploaded — press Sync sources to import it.`` — BYTE-IDENTICAL to the pre-phase-154 string; * a legacy 2-arg call (``when`` undefined) renders the pre-phase nameful string (the fallback is falsy-driven); * the non-``uploaded`` detail and a missing detail keep the unchanged ``The upload finished.``.""" js = _gjs() fn = _gfn(js, "fmtUploadResult") script = ( fn + "\n" 'const S = { message: "uploaded" };\n' 'const O = { message: "other" };\n' "console.log(JSON.stringify([\n" ' fmtUploadResult(S, "docs.zip", "21:14"),\n' ' fmtUploadResult(S, "docs.zip", ""),\n' ' fmtUploadResult(S, null, "10/2, 3:14 PM"),\n' ' fmtUploadResult(S, null, ""),\n' ' fmtUploadResult(S, "docs.zip"),\n' ' fmtUploadResult(O, "docs.zip", "21:14"),\n' " fmtUploadResult(null, \"docs.zip\", \"21:14\"),\n" "]));\n" ) rows = json.loads(_run_node(script)) assert rows == [ "Uploaded docs.zip (21:14) — press Sync sources to import it.", # byte-identical to pre-phase (the no-stamp nameful variant): "Uploaded docs.zip — press Sync sources to import it.", "Uploaded 10/2, 3:14 PM — press Sync sources to import it.", # byte-identical to pre-phase (the no-stamp nameless variant): "Uploaded — press Sync sources to import it.", # the legacy 2-arg call (when undefined → falsy → pre-phase): "Uploaded docs.zip — press Sync sources to import it.", "The upload finished.", "The upload finished.", ], rows # ===================================================================== # Phase 154 (task 03) — the byte-identity pins (the pre-phase # strings, pinned verbatim — assertable, not assumed) # ===================================================================== #: The pre-phase-154 fmtUploadResult, pinned VERBATIM from git #: history (the 2-arg no-count formatter — the phase-90 contract #: pre-stamp). Only the function NAME is renamed (so the real + the #: reference run side by side under node); the body is the old code #: byte-for-byte — the two old line strings + "The upload finished." #: live in this constant, so the byte-identity below is assertable, #: not assumed. PRE_PHASE_FMT_UPLOAD_RESULT_JS = """ function prePhaseUploadResult(detail, name) { if (detail && detail.message === "uploaded") { return name ? `Uploaded ${name} — press Sync sources to import it.` : "Uploaded — press Sync sources to import it."; } return "The upload finished."; } """ #: The two pre-phase-154 result-line strings, pinned verbatim as #: constants (the byte-identity targets — the task-03 completion #: criterion). The nameful one is the old template instantiated with #: the test's input name. PRE_PHASE_UPLOAD_LINE_NAMEFUL = "Uploaded docs.zip — press Sync sources to import it." PRE_PHASE_UPLOAD_LINE_NAMELESS = "Uploaded — press Sync sources to import it." @pytest.mark.skipif(not HAVE_NODE, reason="node not available") def test_upload_result_no_stamp_is_byte_identical_to_pre_phase() -> None: """Phase 154 (task 03): the two BYTE-IDENTITY fallbacks — the REAL 3-arg ``fmtUploadResult`` and the VERBATIM pre-phase 2-arg formatter (pinned above) run side by side under node (clock-free — ``when`` is precomputed): * name + ``when === ""`` → the pre-phase nameful string, byte-identical (the running / legacy / partial payload); * no name + ``when === ""`` → the pre-phase nameless string, byte-identical (the same payload, the name unknown); * the legacy 2-arg call (``when`` undefined → falsy) renders the SAME two byte-identical strings (the old call shape is unchanged in output); * the non-``uploaded`` detail (and a missing detail) keep ``The upload finished.`` on BOTH sides (the branch the phase never touched). The reference implementation's own output is asserted against the verbatim old strings FIRST (a self-check on the pinned constant — a typo in the pinned function fails here, not silently), so the real-vs-reference identity is an assertion against the verbatim pre-phase strings (the completion criterion), not an assumption.""" # Source-level self-check: the pinned constant carries the old # strings verbatim (both line templates + the non-uploaded # branch). assert ( "return name\n" " ? `Uploaded ${name} — press Sync sources to import it.`\n" ' : "Uploaded — press Sync sources to import it.";' in PRE_PHASE_FMT_UPLOAD_RESULT_JS ), "the old nameful + nameless line templates are pinned verbatim" assert '"The upload finished."' in PRE_PHASE_FMT_UPLOAD_RESULT_JS, ( "the non-uploaded branch is pinned verbatim" ) fn = _gfn(_gjs(), "fmtUploadResult") script = ( fn + "\n" + PRE_PHASE_FMT_UPLOAD_RESULT_JS + "\n" 'const S = { message: "uploaded" };\n' 'const O = { message: "other" };\n' "console.log(JSON.stringify({\n" " REAL: [\n" ' fmtUploadResult(S, "docs.zip", ""),\n' " fmtUploadResult(S, null, \"\"),\n" ' fmtUploadResult(S, "docs.zip"),\n' # legacy 2-arg call " fmtUploadResult(S, null),\n" # legacy 2-arg call ' fmtUploadResult(O, "docs.zip", "21:14"),\n' ' fmtUploadResult(null, "docs.zip", "21:14"),\n' " ],\n" " PRE: [\n" ' prePhaseUploadResult(S, "docs.zip"),\n' " prePhaseUploadResult(S, null),\n" ' prePhaseUploadResult(O, "docs.zip"),\n' ' prePhaseUploadResult(null, "docs.zip"),\n' " ]\n" "}));\n" ) rows = json.loads(_run_node(script)) # 1. the pinned reference reproduces the verbatim old strings # (self-check on the constant — assertable, not assumed). assert rows["PRE"] == [ PRE_PHASE_UPLOAD_LINE_NAMEFUL, PRE_PHASE_UPLOAD_LINE_NAMELESS, "The upload finished.", "The upload finished.", ], rows # 2. the no-stamp + legacy shapes are byte-identical to the # reference (i.e. to the verbatim old strings). assert rows["REAL"][:4] == [ PRE_PHASE_UPLOAD_LINE_NAMEFUL, # name + when === "" PRE_PHASE_UPLOAD_LINE_NAMELESS, # no name + when === "" PRE_PHASE_UPLOAD_LINE_NAMEFUL, # legacy 2-arg call PRE_PHASE_UPLOAD_LINE_NAMELESS, # legacy 2-arg call ], rows assert rows["REAL"][0] == rows["PRE"][0], ( "the nameful no-stamp shape is byte-identical to pre-phase" ) assert rows["REAL"][1] == rows["PRE"][1], ( "the nameless no-stamp shape is byte-identical to pre-phase" ) # 3. the non-uploaded branch is unchanged on both sides. assert rows["REAL"][4] == rows["PRE"][2] == "The upload finished." assert rows["REAL"][5] == rows["PRE"][3] == "The upload finished." #: Phase 154 (task 02): the fixed clock for the stamp's boundary set — #: the SAME fixed now as test_sync_button.py's task-01 pin (Oct 3, #: 2026, 18:30 LOCAL). The test body never calls `new Date()` for its #: own timing: the fake clock is installed in the driver (global Date #: patched BEFORE the call — `new Date()` → the fixed now, #: `new Date(iso)` → the real parse), and every ISO is #: designator-less (local) so the comparison is #: timezone-independent. _UPLOAD_STAMP_NOW_JS = "new RealDate(2026, 9, 3, 18, 30, 0)" def _upload_stamp_driver(fn_src: str, expr: str) -> str: """Run `expr` under node with the REAL git-sources.js function + the fake clock (the idiom: the Date patch precedes the call).""" driver = ( f"{fn_src}\n" "const RealDate = Date;\n" f"const NOW = {_UPLOAD_STAMP_NOW_JS};\n" "globalThis.Date = function (...args) {\n" " return args.length === 0 ? NOW : new RealDate(...args);\n" "};\n" f"{expr}\n" ) proc = subprocess.run( ["node", "-e", driver], capture_output=True, text=True, timeout=60 ) assert proc.returncode == 0, f"node failed: {proc.stderr}" return proc.stdout @pytest.mark.skipif(not HAVE_NODE, reason="node not available") def test_upload_stamp_behavior_fixed_clock() -> None: """Phase 154 (task 02): the REAL git-sources.js ``fmtUploadStamp`` under node (fake clock, the fixed now Oct 3 2026 18:30 local — the same clock as test_sync_button.py's task-01 pin): finished wins over started; started when finished is missing/null; ``""`` when both are absent (or the status object is missing — the defensive guard); a garbage ISO → ``""`` with no fallback (and a corrupt finished_at degrades to the started_at stamp when a started_at is present — the defensive guard); the A1 date-qualifier's boundary set (same day ``HH:MM`` — the audit's own ``21:14`` case + the end-of-day edge; yesterday ``M/D, h:MM AM/PM`` with the ``12:05 AM`` edge; last year ``M/D/YYYY, h:MM AM/PM``). The wall clock never appears in an assertion.""" fn = _gfn(_gjs(), "fmtUploadStamp") def _lit(status: object) -> str: if status is None: return "null" assert isinstance(status, dict) parts = [] for k, v in status.items(): if v is None: parts.append(f"{k}: null") else: parts.append(f'{k}: "{v}"') return "{" + ", ".join(parts) + "}" # (status, expected) — the expected strings are the pins. cases = [ ( {"finished_at": "2026-10-03T16:06:00", "started_at": "2026-10-03T16:05:00"}, "16:06", # finished wins over started ), ({"started_at": "2026-10-02T15:14:00"}, "10/2, 3:14 PM"), # no finished → started ( {"finished_at": None, "started_at": "2025-10-03T21:12:00"}, "10/3/2025, 9:12 PM", # null finished → started, last year ), ({"finished_at": "garbage"}, ""), # unparseable, no fallback ( {"finished_at": "garbage", "started_at": "2026-10-03T16:06:00"}, "16:06", # corrupt finished degrades to the started_at stamp ), ({"finished_at": None, "started_at": None}, ""), # both absent ({}, ""), # both missing (None, ""), # the status object itself missing (the guard) ({"finished_at": "2026-10-02T00:05:00"}, "10/2, 12:05 AM"), # 12:05 AM edge ({"finished_at": "2026-10-03T23:59:00"}, "23:59"), # same day, end-of-day ] driver_expr = ( "const CASES = [" + ", ".join(_lit(s) for s, _ in cases) + "];\n" "const STAMPS = CASES.map((c) => fmtUploadStamp(c));\n" "console.log(JSON.stringify(STAMPS));" ) out = json.loads(_upload_stamp_driver(fn, driver_expr)) assert out == [expected for _, expected in cases], out