Вики: одна синхронизация на корень — повтор не ставит вторую (#135, T8.3)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-27 18:41:21 +04:00
parent 6cc4b9a6b9
commit 39821b8ee6
3 changed files with 85 additions and 3 deletions

View File

@ -47,16 +47,38 @@ def validate(cmds) -> list[dict]:
return out
def _active_same_root(conn, label: str, root: str) -> int | None:
"""id самого нового пакета проекта с wiki_sync того же корня в queued/claimed или None."""
row = conn.execute(
"SELECT batch_id FROM command WHERE type = 'wiki_sync' AND status IN ('queued', 'claimed') "
"AND json_extract(params, '$.label') = ? AND json_extract(params, '$.root') = ? ORDER BY batch_id DESC LIMIT 1",
(label, root)).fetchone()
return row["batch_id"] if row else None
def check_batch(conn, cmds, project, files_dir) -> list[dict]:
"""wiki_sync живёт в пакете своего проекта: project пакета равен label команды."""
"""wiki_sync живёт в пакете своего проекта: project пакета равен label команды. Вторая живая синхронизация того же
корня не ставится — ни через POST /batches, ни повтором команды (T8.3); другой корень не мешает (смена корня)."""
for i, c in enumerate(cmds, 1):
if c["type"] == "wiki_sync" and c["params"]["label"] != project:
if c["type"] != "wiki_sync":
continue
if c["params"]["label"] != project:
raise CommandError(f"команда {i}: проект пакета ({project}) не равен label ({c['params']['label']})")
bid = _active_same_root(conn, project, c["params"]["root"])
if bid is not None:
raise CommandError(f"команда {i}: у проекта {project} уже идёт синхронизация корня {c['params']['root']} "
f"(пакет #{bid})")
return cmds
def enqueue_sync(conn, label: str, root: str, author: str) -> dict:
"""Пакет «Вики <метка>» из одной команды wiki_sync."""
"""Пакет «Вики <метка>» из одной команды wiki_sync. Идёт синхронизация того же корня (возврат корня A → B → A,
пока A у агента) — её пакет в той же форме {id, estimate_seconds, count}, нового нет."""
bid = _active_same_root(conn, label, root)
if bid is not None:
row = conn.execute("SELECT b.estimate_seconds, (SELECT COUNT(*) FROM command c WHERE c.batch_id = b.id) AS n "
"FROM batch b WHERE b.id = ?", (bid,)).fetchone()
return {"id": bid, "estimate_seconds": row["estimate_seconds"], "count": row["n"]}
return queue.create_batch(conn, f"Вики {label}", author, [{"type": "wiki_sync", "params": {"label": label, "root": root}}],
project=label)

View File

@ -128,3 +128,32 @@ def test_validate_settings(key, bad, good):
def test_wiki_agent_gets_own_keys(conn):
out = settings.for_agent(conn, "wiki")
assert out == {"wiki_pacing": 0.2, "settings_version": 1}
# пакет D (#135): T8.3 — одна синхронизация на корень
SYNC = [{"type": "wiki_sync", "params": {"label": "AWPCA", "root": "DOC-1"}}]
def test_check_batch_refuses_second_sync_same_root(conn):
b = queue.create_batch(conn, "t", "u", SYNC, project="AWPCA")
with pytest.raises(CommandError, match=f"#{b['id']}"):
queue.create_batch(conn, "t", "u", SYNC, project="AWPCA")
def test_check_batch_other_root_allowed(conn):
queue.create_batch(conn, "t", "u", SYNC, project="AWPCA")
other = [{"type": "wiki_sync", "params": {"label": "AWPCA", "root": "DOC-2"}}]
assert queue.create_batch(conn, "t", "u", other, project="AWPCA")["count"] == 1
def test_retry_failed_sync_without_active_passes(conn):
b = queue.create_batch(conn, "t", "u", SYNC, project="AWPCA")
conn.execute("UPDATE command SET status = 'failed' WHERE batch_id = ?", (b["id"],))
assert queue.create_batch(conn, "t", "u", SYNC, project="AWPCA")["count"] == 1
def test_enqueue_sync_returns_active_same_root(conn):
_root(conn)
first = commands.enqueue_sync(conn, "AWPCA", "DOC-1", "u")
again = commands.enqueue_sync(conn, "AWPCA", "DOC-1", "u")
assert again == first and _count(conn) == 1

View File

@ -190,3 +190,34 @@ def test_create_project_batches(hub_server):
c.post("/projects", {"label": "AWPCA", "name": "A"})
patched = c.patch("/projects/AWPCA", {"wiki_root": "DOC-000777"})
assert patched["batches"] == [{"module": "wiki", "id": patched["batch_ids"][0]}]
def test_root_back_while_old_claimed(hub_server):
"""A → B → A, пока синхронизация A у агента: PATCH не 422, идущий пакет A остаётся (#135, T8.3)."""
c = hub_server["client"]
c.post("/projects", {"label": "AWPCA", "name": "АРМ КА", "wiki_root": "DOC-12"})
conn = hub_server["hub"].conn()
try:
conn.execute("UPDATE command SET status = 'claimed' WHERE type = 'wiki_sync'")
finally:
conn.close()
c.patch("/projects/AWPCA", {"wiki_root": "DOC-21"})
c.patch("/projects/AWPCA", {"wiki_root": "DOC-12"})
assert [(s, p["root"]) for _, s, p in _syncs(hub_server["hub"])] == [("claimed", "DOC-12"), ("cancelled", "DOC-21")]
def test_retry_while_sync_running_refused(hub_server):
"""Повтор упавшей wiki_sync при идущей того же корня — 422 с номером идущего пакета (#135, T8.3)."""
c = hub_server["client"]
c.post("/projects", {"label": "AWPCA", "name": "АРМ КА", "wiki_root": "DOC-1"})
conn = hub_server["hub"].conn()
try:
failed = conn.execute("SELECT id FROM command WHERE type = 'wiki_sync'").fetchone()["id"]
conn.execute("UPDATE command SET status = 'failed' WHERE id = ?", (failed,))
finally:
conn.close()
running = c.post("/wiki/sync", {"project": "AWPCA"})["batch_id"]
with pytest.raises(HubError) as e:
c.post(f"/commands/{failed}/retry", {})
assert e.value.status == 422 and f"#{running}" in str(e.value)
assert len(_syncs(hub_server["hub"])) == 2