phase: 152_sync_progress_image_pass
All criteria verified. The CLI path is hook-less (only `app/api/sync.py` passes the hook), and the byte-identity is pinned by `test_on_image_progress_none_run_is_byte_identical`. ## Final Report — Phase 152 final verification pass - **Verified (no new code needed):** `SyncStatus.images_done/images_total` + reset/clear rules in `app/api/sync.py`; `on_image_progress` threaded through the importer's three PDF vision spans with the continuous per-file counter; the `_image_hook` closure; `sources.js` `· image n/m` label part (box + title + aria). - **C1** status climbs 1→N + UI shows `· image n/m`: ✅ unit 3/15→15/15, integration 1/15→7/15, E2E 1/4→4/4 (title/aria ride the full string) - **C2** never frozen: ✅ E2E asserts ≥3 distinct positions, starting at 1, non-decreasing, ≤ total, reaching it - **C3** cleared at overview/summaries + all terminals; per-file reset via `_hook`; no stale position for image-less files: ✅ unit pins + E2E settle (image part gone in box/title/aria, endpoint 0/0) - **C4** `/api/sync/status` additive-only: ✅ full-dict pin passes (all pre-existing keys unchanged + the two new 0/0 keys) - **C5** hook-less caller byte-identical: ✅ CLI passes no hook; `test_on_image_progress_none_run_is_byte_identical` green - **C6** `uv run pytest`: **3526 passed, exit 0** (95s); dedicated E2E in isolation: **2 passed** (95s); `uv run ruff check . && uv run pyright`: **clean**; coverage: **99% on `app/` (>90% gate)** - Regression: all five neighbouring E2E suites green in isolation (progress_box 8, chunk_progress 2, label_mobile 4, pdf_support 6, image_documents 4) - **C7** atomic commit: left to the harness (no git index/history touched per rules) - **Docs:** README.md — sync-label paragraph already updated this phase, accurately; all `.env.example` vars documented; routes/commands verified — **no changes needed**. AGENTS.md — all references (PLAN, user_stories, test_smoke.py, .env.example, gpg config, .gitignore) verified — **no changes needed** - **Defects found in prior work:** none (one prior env-leak defect was already fixed inside this phase's task 04, conftest) - No live-infrastructure touched; no ntfy used. **Next pending phase:** none — `todo/` held only this phase
This commit is contained in:
@@ -93,6 +93,7 @@ import asyncio
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import textwrap
|
||||
import threading
|
||||
import time
|
||||
from collections.abc import Callable, Iterator, Sequence
|
||||
@@ -100,6 +101,7 @@ from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pymupdf as fitz
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy import select, text
|
||||
@@ -309,6 +311,12 @@ class FakeImportSources:
|
||||
# regresses) — the canned fake never fires it (the runner's
|
||||
# status chunk fields stay 0/0, like a no-batch import).
|
||||
self.chunk_hooks: list[object] = []
|
||||
# Phase 152 (task 03): the image-position hook the runner
|
||||
# passes (a live closure while wired, None if the wiring
|
||||
# regresses) — the canned fake never fires it (the status
|
||||
# image fields stay 0/0, like the chunk fields of a no-batch
|
||||
# import).
|
||||
self.image_hooks: list[object] = []
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
@@ -320,6 +328,7 @@ class FakeImportSources:
|
||||
session: Session | None = None,
|
||||
progress: Callable[[str, str, int, int], None] | None = None,
|
||||
on_chunk_progress: Callable[[int, int], None] | None = None, # phase 129
|
||||
on_image_progress: Callable[[int, int], None] | None = None, # phase 152
|
||||
ignore_by_root: dict[str, list[str]] | None = None,
|
||||
include_hidden_by_root: dict[str, bool] | None = None,
|
||||
doc_dates_by_root: dict[str, dict[str, datetime]] | None = None, # phase 106
|
||||
@@ -329,6 +338,7 @@ class FakeImportSources:
|
||||
self.prune_flags.append(prune)
|
||||
self.progress_hooks.append(progress)
|
||||
self.chunk_hooks.append(on_chunk_progress)
|
||||
self.image_hooks.append(on_image_progress)
|
||||
self.ignore_maps.append(ignore_by_root or {})
|
||||
self.include_hidden_maps.append(include_hidden_by_root or {})
|
||||
self.doc_dates_maps.append(doc_dates_by_root or {})
|
||||
@@ -500,6 +510,10 @@ def test_admin_sync_success_reports_full_detail(
|
||||
# (the dataclass defaults).
|
||||
"current_chunk": 0,
|
||||
"chunks_total": 0,
|
||||
# Phase 152 (task 01): the image-position keys — 0/0 idle
|
||||
# (the dataclass defaults).
|
||||
"images_done": 0,
|
||||
"images_total": 0,
|
||||
# Phase 98 (task 01): the phase-machine keys — null/0/0/0 idle.
|
||||
"phase": None,
|
||||
"current_summary": None,
|
||||
@@ -656,13 +670,23 @@ def test_double_trigger_while_running_returns_409(
|
||||
class _GatedImport:
|
||||
"""An import that fires the runner's progress hook once, then
|
||||
parks on a threading gate — the test reads the status mid-import
|
||||
(the phase ``"import"`` + the file hook)."""
|
||||
(the phase ``"import"`` + the file hook).
|
||||
|
||||
Phase 152 (task 03): optionally ALSO fires the in-flight file's
|
||||
image-position pairs (``image_pairs``) through the runner's OWN
|
||||
``on_image_progress`` closure: pair one lands before the main park
|
||||
(readable mid-run), every later pair after the main park + its own
|
||||
park (``image_release``), so each position is readable on the
|
||||
status between polls (the phase-129 chunk idiom, mirrored for the
|
||||
vision-image position)."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
summary: ImportSummary,
|
||||
started: threading.Event,
|
||||
release: threading.Event,
|
||||
image_pairs: Sequence[tuple[int, int]] = (),
|
||||
image_release: threading.Event | None = None,
|
||||
) -> None:
|
||||
self.summary = summary
|
||||
self.started = started
|
||||
@@ -672,6 +696,13 @@ class _GatedImport:
|
||||
# passes (recorded for the wiring pin — the gated fake never
|
||||
# fires it, so the status chunk fields stay 0/0).
|
||||
self.chunk_hook: Callable[[int, int], None] | None = None
|
||||
# Phase 152 (task 03): the image-position hook the runner
|
||||
# passes (recorded for the wiring pin) + the optional image
|
||||
# pairs (see the class docstring).
|
||||
self.image_hook: Callable[[int, int], None] | None = None
|
||||
self.image_pairs = list(image_pairs)
|
||||
self.image_release = image_release
|
||||
self.image_hook_calls: list[tuple[int, int]] = []
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
@@ -683,6 +714,7 @@ class _GatedImport:
|
||||
session: Session | None = None,
|
||||
progress: Callable[[str, str, int, int], None] | None = None,
|
||||
on_chunk_progress: Callable[[int, int], None] | None = None, # phase 129
|
||||
on_image_progress: Callable[[int, int], None] | None = None, # phase 152
|
||||
ignore_by_root: dict[str, list[str]] | None = None,
|
||||
include_hidden_by_root: dict[str, bool] | None = None,
|
||||
doc_dates_by_root: dict[str, dict[str, datetime]] | None = None, # phase 106
|
||||
@@ -691,6 +723,25 @@ class _GatedImport:
|
||||
progress("repo", "notes/deep.md", 1, 3)
|
||||
self.hook_calls.append(("repo", "notes/deep.md", 1, 3))
|
||||
self.chunk_hook = on_chunk_progress
|
||||
self.image_hook = on_image_progress
|
||||
if on_image_progress is not None and self.image_pairs:
|
||||
# Phase 152 (task 03): the in-flight PDF's image-position
|
||||
# pairs, fired through the runner's OWN hook closure (the
|
||||
# closure under test) — pair one before the main park, each
|
||||
# later pair after the main park + its own park (each park
|
||||
# lets the test read the status between polls). A wiring
|
||||
# regression to None skips this branch and the pair test
|
||||
# times out.
|
||||
on_image_progress(*self.image_pairs[0])
|
||||
self.image_hook_calls.append(self.image_pairs[0])
|
||||
self.started.set()
|
||||
await asyncio.to_thread(self.release.wait)
|
||||
for pair in self.image_pairs[1:]:
|
||||
on_image_progress(*pair)
|
||||
self.image_hook_calls.append(pair)
|
||||
if self.image_release is not None:
|
||||
await asyncio.to_thread(self.image_release.wait)
|
||||
return self.summary
|
||||
self.started.set()
|
||||
await asyncio.to_thread(self.release.wait)
|
||||
return self.summary
|
||||
@@ -867,6 +918,305 @@ def test_status_reports_the_phase_machine_across_the_run(
|
||||
assert gated_import.hook_calls == [("repo", "notes/deep.md", 1, 3)]
|
||||
|
||||
|
||||
def test_image_position_climbs_between_polls_mid_run(
|
||||
sync_client: TestClient, monkeypatch: pytest.MonkeyPatch, db: Session, tmp_path: Path
|
||||
) -> None:
|
||||
"""Phase 152 (task 03): while a PDF's vision span is in flight,
|
||||
the status carries the in-flight image position — the pairs are
|
||||
fired through the runner's OWN ``_image_hook`` closure (task 03's
|
||||
wiring: ``import_sources(..., on_image_progress=_image_hook)``),
|
||||
and the position CLIMBS between two polls of
|
||||
``GET /api/sync/status`` (1/15 → 7/15 — monotonic, never above
|
||||
the total, no backwards jumps — the owner's stuck label now
|
||||
moves), paired with the file the ``progress`` hook named (the
|
||||
hooks are paired — the position belongs to that file); the
|
||||
success terminal clears the position to 0/0 while the FILE
|
||||
counts keep their final values (the convention split — same as
|
||||
the chunk position)."""
|
||||
repo_url = f"file://{tmp_path / 'repo.git'}"
|
||||
_seed(db, repo_url)
|
||||
_stub_env(monkeypatch)
|
||||
monkeypatch.setattr(
|
||||
sync_api,
|
||||
"get_settings",
|
||||
lambda: _settings(sources_dir=str(tmp_path / "bor")),
|
||||
)
|
||||
_, fake_clone = _fake_clone()
|
||||
monkeypatch.setattr(sync_api, "clone_or_pull", fake_clone)
|
||||
_stub_probe(monkeypatch)
|
||||
image_release = threading.Event()
|
||||
gated_import = _GatedImport(
|
||||
ImportSummary(files=3, added=1, updated=1, unchanged=1),
|
||||
threading.Event(),
|
||||
threading.Event(),
|
||||
image_pairs=[(1, 15), (7, 15)], # two describe calls of a 15-image span
|
||||
image_release=image_release,
|
||||
)
|
||||
monkeypatch.setattr(sync_api, "import_sources", gated_import)
|
||||
monkeypatch.setattr(sync_api, "regenerate_overview", FakeOverview(ok=True))
|
||||
monkeypatch.setattr(sync_api, "generate_folder_summaries", FakeFolderSummaries())
|
||||
|
||||
_login(sync_client)
|
||||
# Idle: the image keys ride along as 0/0 (the dataclass defaults).
|
||||
body = sync_client.get("/api/sync/status").json()
|
||||
assert body["state"] == "idle"
|
||||
assert body["images_done"] == 0 and body["images_total"] == 0
|
||||
|
||||
assert sync_client.post("/api/sync").status_code == 202
|
||||
try:
|
||||
assert gated_import.started.wait(5.0), "the run never reached the first image pair"
|
||||
# Poll 1 — the import phase, pair one live: the position is
|
||||
# 1/15, paired with the file the progress hook named; the
|
||||
# chunk position is 0/0 (this canned file has no embed
|
||||
# batches — the two positions are independent).
|
||||
s = sync_client.get("/api/sync/status").json()
|
||||
assert s["state"] == "running"
|
||||
assert s["phase"] == "import"
|
||||
assert s["current_file"] == "repo/notes/deep.md"
|
||||
assert s["files_done"] == 1 and s["files_total"] == 3
|
||||
assert s["current_chunk"] == 0 and s["chunks_total"] == 0
|
||||
assert s["images_done"] == 1 and s["images_total"] == 15
|
||||
# Release pair one: the import fires pair two (7/15 — the
|
||||
# NEXT describe call of the same 15-image span) and parks
|
||||
# again.
|
||||
gated_import.release.set()
|
||||
deadline = time.monotonic() + 5.0
|
||||
while time.monotonic() < deadline:
|
||||
s = sync_client.get("/api/sync/status").json()
|
||||
if s["images_done"] == 7:
|
||||
break
|
||||
assert s["state"] == "running", (
|
||||
f"unexpected state {s['state']!r} while waiting for pair two: {s}"
|
||||
)
|
||||
time.sleep(0.05)
|
||||
else:
|
||||
pytest.fail("the second image pair never landed on the status")
|
||||
# Poll 2 — the position CLIMBED (1 → 7, monotonic — no
|
||||
# backwards jump, never above the total): the label is
|
||||
# moving, not frozen.
|
||||
assert s["state"] == "running"
|
||||
assert s["phase"] == "import"
|
||||
assert s["current_file"] == "repo/notes/deep.md"
|
||||
assert s["images_done"] == 7 and s["images_total"] == 15
|
||||
assert 0 < s["images_done"] <= s["images_total"]
|
||||
image_release.set()
|
||||
finally:
|
||||
gated_import.release.set()
|
||||
image_release.set()
|
||||
|
||||
body = _poll(sync_client, "success")
|
||||
# Terminal: the image position clears (0/0) ... the FILE counts
|
||||
# KEEP their final values (the convention split — the position is
|
||||
# a sub-position of a done file, like the chunk position).
|
||||
assert body["images_done"] == 0 and body["images_total"] == 0
|
||||
assert body["current_file"] is None
|
||||
assert body["files_done"] == 1 and body["files_total"] == 3
|
||||
# Wiring: BOTH pairs went through the runner's OWN live closure
|
||||
# (the status reads above came through it — a wiring regression
|
||||
# to None would have skipped the firing branch and timed out),
|
||||
# and the file hook fired once.
|
||||
assert gated_import.image_hook is not None
|
||||
assert gated_import.image_hook_calls == [(1, 15), (7, 15)]
|
||||
assert gated_import.hook_calls == [("repo", "notes/deep.md", 1, 3)]
|
||||
|
||||
|
||||
# --- phase 152 (task 04): the full-run image-position monotonicity -------
|
||||
|
||||
|
||||
def _kb_solid_png(value: int) -> bytes:
|
||||
"""A 40x30 solid-color PNG (channel *value*): every value used
|
||||
below stays under the phase-151 blank gate's 235 mean floor
|
||||
(kept, never blank-skipped), and distinct values are distinct
|
||||
images (the parser's dedupe keeps every one)."""
|
||||
pix = fitz.Pixmap(fitz.csRGB, fitz.IRect(0, 0, 40, 30), 0)
|
||||
pix.clear_with(value)
|
||||
return pix.tobytes("png")
|
||||
|
||||
|
||||
def _kb_image_pdf(path: Path, values: Sequence[int]) -> None:
|
||||
"""A REAL one-page PDF (the fixture KB's image-bearing file): a
|
||||
text block comfortably over the phase-140 100-char sparseness
|
||||
bar (a USABLE layer — the phase-133 gate stays closed, zero
|
||||
pictorial candidates even with the toggle on — the "no other
|
||||
vision span" class) carrying *values* the solid-color embedded
|
||||
images (one placement each, distinct)."""
|
||||
doc = fitz.open()
|
||||
page = doc.new_page()
|
||||
text = (
|
||||
"The quadlet unit file defines the container interface, the "
|
||||
"image, the network, and the restart policy. Every key maps "
|
||||
"to a docker flag in the generated service file, and comments "
|
||||
"use the usual semicolon form throughout the whole document."
|
||||
)
|
||||
for i, line in enumerate(textwrap.wrap(text, 60)):
|
||||
page.insert_text((72, 72 + i * 14), line)
|
||||
y = 300
|
||||
for value in values:
|
||||
page.insert_image(fitz.Rect(72, y, 112, y + 30), stream=_kb_solid_png(value))
|
||||
y += 40
|
||||
doc.save(path)
|
||||
doc.close()
|
||||
|
||||
|
||||
class _GatedVisionLLM(FakeEmbedder):
|
||||
"""The phase-152 (task 04) mock LLM — the deterministic
|
||||
in-process ``FakeEmbedder`` whose VISION calls (the multimodal
|
||||
describe shape ``describe_image`` sends) PARK on a threading
|
||||
gate: the run parks inside the PDF's embedded-image span, so the
|
||||
test can read the status between the describe calls
|
||||
(deterministic — no sleeps, no wall-clock dependence; the gate
|
||||
holds the run until the test releases it). The hook fired BEFORE
|
||||
the parked call (the task-02 contract), so the position is live
|
||||
while the run is parked. Text (``lite``) calls keep the
|
||||
``FakeEmbedder`` behaviour; the embeds are the deterministic
|
||||
vectors."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.vision_parks = 0 # the vision calls that parked (and released)
|
||||
self.vision_release = threading.Event()
|
||||
|
||||
async def chat(
|
||||
self, messages: list[dict[str, Any]], model: str | None = None
|
||||
) -> str:
|
||||
user = next((m["content"] for m in messages if m.get("role") == "user"), "")
|
||||
if isinstance(user, list):
|
||||
# One slow vision describe call — the gate (clear before
|
||||
# the wait — a latching set would otherwise leak across
|
||||
# calls): the test releases after reading the position.
|
||||
self.vision_release.clear()
|
||||
self.vision_parks += 1
|
||||
await asyncio.to_thread(self.vision_release.wait)
|
||||
return "A solid color swatch described." # deterministic, non-empty
|
||||
return await super().chat(messages, model)
|
||||
|
||||
|
||||
def test_image_position_monotonic_across_polls_over_a_real_pdf(
|
||||
sync_client: TestClient,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
db: Session,
|
||||
tmp_path: Path,
|
||||
clean_documents: None,
|
||||
) -> None:
|
||||
"""Phase 152 (task 04): a FULL sync run over a fixture KB with
|
||||
ONE image-bearing PDF (the REAL ``import_sources`` — the
|
||||
runner's ``_image_hook`` is driven by the importer's own
|
||||
embedded-image span; the LLM is the deterministic mock whose
|
||||
vision calls park on a gate) — polling ``GET /api/sync/status``
|
||||
from the test while the run is parked between the describe
|
||||
calls, the observed ``images_done`` sequence is MONOTONICALLY
|
||||
NON-DECREASING, never above ``images_total``, and ends at
|
||||
``images_total`` (no backwards jumps, no overshoot — the
|
||||
owner's stuck label climbs one describe call at a time); the
|
||||
position is paired with the file the ``progress`` hook named
|
||||
(the hooks are paired), the success terminal clears it to 0/0
|
||||
while the FILE counts keep their final values, and the KB
|
||||
carries the PDF + its N image docs (the span really ran)."""
|
||||
kb = tmp_path / "PictKB"
|
||||
kb.mkdir()
|
||||
(kb / "notes.md").write_text("# Notes\nthe local fixture\n", encoding="utf-8")
|
||||
_kb_image_pdf(kb / "report.pdf", (90, 130, 170)) # 3 distinct images
|
||||
n = 3
|
||||
_seed_local(db, kb)
|
||||
_stub_env(monkeypatch)
|
||||
monkeypatch.setattr(
|
||||
sync_api,
|
||||
"get_settings",
|
||||
lambda: _settings(sources_dir=str(tmp_path / "bor")),
|
||||
)
|
||||
clone_calls, fake_clone = _fake_clone()
|
||||
monkeypatch.setattr(sync_api, "clone_or_pull", fake_clone)
|
||||
llm = _GatedVisionLLM()
|
||||
llm.settings = Settings(
|
||||
_env_file=None, # pyright: ignore[reportCallIssue]
|
||||
import_extensions="md,txt,pdf",
|
||||
images=True,
|
||||
image_dir=str(tmp_path / "images"),
|
||||
pdf_page_dir=str(tmp_path / "pages"),
|
||||
)
|
||||
monkeypatch.setattr(sync_api, "LLMClient", lambda: llm)
|
||||
_stub_probe(monkeypatch) # the probe passes on the mock (no network)
|
||||
|
||||
_login(sync_client)
|
||||
|
||||
def status() -> dict:
|
||||
return sync_client.get("/api/sync/status").json()
|
||||
|
||||
def wait_for(predicate, what: str, timeout: float = 10.0) -> None:
|
||||
deadline = time.monotonic() + timeout
|
||||
while time.monotonic() < deadline:
|
||||
if predicate():
|
||||
return
|
||||
time.sleep(0.02)
|
||||
raise AssertionError(f"timed out waiting for {what}")
|
||||
|
||||
# Idle: the image keys ride along as 0/0 (the dataclass
|
||||
# defaults).
|
||||
body = status()
|
||||
assert body["state"] == "idle"
|
||||
assert body["images_done"] == 0 and body["images_total"] == 0
|
||||
|
||||
assert sync_client.post("/api/sync").status_code == 202
|
||||
try:
|
||||
# The run reaches the PDF's vision span: the first hook fired
|
||||
# (total = the span's N) and the first describe call parked
|
||||
# the run (the gate).
|
||||
wait_for(
|
||||
lambda: status()["state"] == "running"
|
||||
and status()["phase"] == "import"
|
||||
and status()["images_total"] == n,
|
||||
"the run to reach the PDF's vision span",
|
||||
)
|
||||
# Each describe call: the hook fired BEFORE the call, so the
|
||||
# position is live while the run is parked in the call —
|
||||
# observe it, release the call, let the next position climb.
|
||||
observed: list[tuple[int, int]] = []
|
||||
for expected in range(1, n + 1):
|
||||
def position_ready(target: int = expected) -> bool:
|
||||
# the default argument binds the loop variable (B023)
|
||||
return status()["images_done"] == target
|
||||
|
||||
wait_for(position_ready, f"images_done to climb to {expected}")
|
||||
s = status()
|
||||
assert s["state"] == "running" and s["phase"] == "import"
|
||||
# The position is paired with the file the progress hook
|
||||
# named (the two hooks are paired — the position belongs
|
||||
# to that file).
|
||||
assert s["current_file"] == "PictKB/report.pdf"
|
||||
observed.append((s["images_done"], s["images_total"]))
|
||||
llm.vision_release.set()
|
||||
# The run finishes the rest of the pipeline (the span is
|
||||
# done — no more vision calls) and settles.
|
||||
assert llm.vision_parks == n, "one park per describe call"
|
||||
finally:
|
||||
llm.vision_release.set()
|
||||
|
||||
body = _poll(sync_client, "success")
|
||||
# The observed sequence: monotonically non-decreasing, never
|
||||
# above the total, ending at the total (the gate made it exact —
|
||||
# 1..N, no backwards jumps, no overshoot).
|
||||
dones = [d for d, _ in observed]
|
||||
assert dones == list(range(1, n + 1)), f"the sequence: {observed}"
|
||||
assert all(d <= t for d, t in observed), "no overshoot"
|
||||
assert dones[-1] == n, "ends at images_total"
|
||||
assert {t for _, t in observed} == {n}, "one total for the whole span"
|
||||
# Terminal: the image position clears (0/0) ... the FILE counts
|
||||
# KEEP their final values (the convention split — the position
|
||||
# is a sub-position of a done file, like the chunk position).
|
||||
assert body["images_done"] == 0 and body["images_total"] == 0
|
||||
assert body["current_file"] is None
|
||||
assert body["files_done"] == 2 and body["files_total"] == 2
|
||||
assert clone_calls == [] # nothing to clone — local is walked directly
|
||||
# The KB carries the fixture md + the PDF + its N image docs —
|
||||
# the span really ran (N new image documents, no failures).
|
||||
assert body["detail"]["added"] == 2 + n
|
||||
assert body["detail"]["errors"] == 0
|
||||
docs = {(d["source"], d["path"])
|
||||
for d in sync_client.get("/api/docs").json()["documents"]}
|
||||
assert ("PictKB", "report.pdf") in docs
|
||||
assert sum(1 for _, p in docs if "#page=" in p) == n
|
||||
|
||||
|
||||
def test_failed_terminal_after_the_summary_hook_clears_phase_keeps_counts(
|
||||
sync_client: TestClient, monkeypatch: pytest.MonkeyPatch, db: Session, tmp_path: Path
|
||||
) -> None:
|
||||
@@ -1497,6 +1847,7 @@ def test_import_error_is_reported_with_credentials_masked(
|
||||
session: Session | None = None,
|
||||
progress: Callable[[str, str, int, int], None] | None = None,
|
||||
on_chunk_progress: Callable[[int, int], None] | None = None, # phase 129
|
||||
on_image_progress: Callable[[int, int], None] | None = None, # phase 152
|
||||
ignore_by_root: dict[str, list[str]] | None = None,
|
||||
include_hidden_by_root: dict[str, bool] | None = None,
|
||||
doc_dates_by_root: dict[str, dict[str, datetime]] | None = None, # phase 106
|
||||
|
||||
Reference in New Issue
Block a user