diff --git a/README.md b/README.md index e2edde2..b05525f 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,9 @@ launchctl unload ~/Library/LaunchAgents/ru.aepif.eva-agent.plist Агент подтянет её сам (`auto_update`); настройку хаба `wiki_min_agent_version` (не `min_agent_version` — та у агента jira) поднимать после того, как `agent_status` покажет новую версию агента wiki. Для v2 (версии страниц): после выкатки -хаба с change `wiki-history` и v2 поднять `wiki_min_agent_version` до 2 (Migration Plan хаба, п. 3). +хаба с change `wiki-history` и v2 поднять `wiki_min_agent_version` до 2 (Migration Plan хаба, п. 3). До этого — проверить +в логе агента первой синхронизации v2 строку «N из M версий без автора»: если она есть, `cmf_author` у версий Eva не +раскрывает, и авторы в истории будут пустыми (их не исправит повторная синхронизация). ## Разработка diff --git a/openspec/changes/wiki-versions/design.md b/openspec/changes/wiki-versions/design.md index fbe0f0e..77280cb 100644 --- a/openspec/changes/wiki-versions/design.md +++ b/openspec/changes/wiki-versions/design.md @@ -9,8 +9,8 @@ - `CmfDocumentHistory.list` с `filter=[["parent_id", "==", "CmfDocument:"]]` работает; `parent.code` — ошибка `-32001` (вложенный фильтр). - Поля версии: `code` (`DOCH-…`), `cmf_created_at` (ISO со смещением `+03:00`), `cmf_author_id`, `text` (полный HTML, - до ~56 тыс. символов), `has_published`, `cur_workflow`. `cmf_author` по имени поля раскрывается в объект с `name` и - `login` (как у `CmfDocument`). + до ~56 тыс. символов), `has_published`, `cur_workflow`. У `CmfDocument` поле `cmf_author` по имени раскрывается в объект с `name` и + `login`; у `CmfDocumentHistory` проба проверяла только `fields: ["**"]` — раскрытие по имени поля не проверено (I-1). - Черновик (`has_published = false`) Eva меняет на месте. ## Goals / Non-Goals @@ -69,3 +69,14 @@ сам. Цена ошибки: нет. - 28.10.2026 — **P-6. e2e с историей — по признаку хаба** (`history_at` в `GET /wiki/status`); с хабом без истории блок пропускается с сообщением. Цена ошибки: до merge hub#140 шаг версий проверяется только юнит-тестами. +- 28.10.2026 — **I-1 (финальное ревью). Версии без автора — строка в лог** «N из M версий без автора (cmf_author не + раскрыт или закрыт ACL)» на синхронизацию. Обоснование: автор известной версии не перечитывается, потеря необратима. + Перед подъёмом `wiki_min_agent_version` — проба `CmfDocumentHistory.list` с `fields=["cmf_author"]` на виртуалке. + Цена ошибки: нет. +- 28.10.2026 — **M-1 (финальное ревью, поднято до Important). Код версии уходит один раз за прогон**, даже если Eva + при сдвиге `slice` отдала запись повторно. Обоснование: хаб считает уникальные коды, повтор давал `failed` + «нет версий: пришло N−1 из N». Цена ошибки: нет. +- 28.10.2026 — **M-3 (финальное ревью, поднято до Important). Нет ключа `text` в ответе `CmfDocumentHistory.get` — + `failed`** «версия X страницы Y: Eva не отдала text (нет прав?)». Уточняет решение 3: пустая версия в хабе не + исправилась бы (код уже известен). `text: null` по-прежнему — пустая строка. Цена ошибки: при закрытом ACL `text` + синхронизация стоит до выдачи прав. diff --git a/src/agent.py b/src/agent.py index 2bf994b..4c4bc37 100644 --- a/src/agent.py +++ b/src/agent.py @@ -981,6 +981,7 @@ class Executor: base = self.eva.base out = _Batcher(lambda b: self._send_batch(self.hub.wiki_versions, command_id, b, "версия", "пачка версий"), MAX_VERSIONS) + no_author = 0 for page_code, doc_id in tree: if doc_id is None: log(" у страницы %s нет id — версии не запрошены" % page_code) @@ -1002,12 +1003,18 @@ class Executor: full = self.eva.get_version(vcode) if not full: raise _VersionMissing("версия %s страницы %s не найдена или нет прав" % (vcode, page_code)) - html = full.get("text") if isinstance(full.get("text"), str) else "" + if "text" not in full: # закрытое поле Eva не отдаёт; пустая версия в хабе не исправилась бы (M-3) + raise _VersionMissing("версия %s страницы %s: Eva не отдала text (нет прав?)" % (vcode, page_code)) + html = full["text"] if isinstance(full["text"], str) else "" attachments, links = classify_links(html, base) - out.add({"page_code": page_code, "code": vcode, "created_at": created, - "author": version_author(v.get("cmf_author")), "html": html, + author = version_author(v.get("cmf_author")) + no_author += author == {"login": None, "name": None} + out.add({"page_code": page_code, "code": vcode, "created_at": created, "author": author, "html": html, "attachments": attachments, "links": links}) + seen.add(vcode) # повтор кода в списке Eva (сдвиг slice) не уходит дважды (M-1) out.flush() + if no_author: # автор известной версии не перечитывается — потерю видно сразу (I-1) + log(" %d из %d версий без автора (cmf_author не раскрыт или закрыт ACL)" % (no_author, out.sent)) return out.sent def do_wiki_fetch(self, p: dict, cmd: dict) -> dict: diff --git a/tests/test_wiki_sync.py b/tests/test_wiki_sync.py index 3caff51..0c7263a 100644 --- a/tests/test_wiki_sync.py +++ b/tests/test_wiki_sync.py @@ -382,6 +382,41 @@ class WikiVersionsTest(SyncBase): self.assertEqual(v1["attachments"], [{"ref": "/files/a/b.png", "url": self.eva.base_url + "/files/a/b.png", "name": "Схема"}]) self.assertEqual((v2["html"], v2["attachments"], v2["links"]), ("", [], [])) + def test_versions_without_author_logged(self): # I-1: потеря автора видна в логе + self.tree2(); e = self.eva + e.add_version("CmfDocument:1", "DOCH-1", author=None) + e.add_version("CmfDocument:1", "DOCH-2", author="CmfPerson:6c2b") + e.add_version("CmfDocument:1", "DOCH-3") + buf = io.StringIO() + with redirect_stdout(buf): + out = self.sync(known_versions={}) + self.assertEqual(out["status"], "done", out) + self.assertIn("2 из 3 версий без автора (cmf_author не раскрыт или закрыт ACL)", buf.getvalue()) + + def test_versions_all_with_author_not_logged(self): + self.tree2(); self.eva.add_version("CmfDocument:1", "DOCH-1") + buf = io.StringIO() + with redirect_stdout(buf): + self.sync(known_versions={}) + self.assertNotIn("без автора", buf.getvalue()) + + def test_versions_duplicate_code_sent_once(self): # M-1: повтор кода в списке Eva + self.tree2() + self.eva.add_version("CmfDocument:1", "DOCH-1", created_at="2026-10-27T10:00:00+03:00") + self.eva.add_version("CmfDocument:1", "DOCH-1", created_at="2026-10-27T10:00:01+03:00") + out = self.sync(known_versions={}) + self.assertEqual(out["status"], "done", out) + self.assertEqual(out["result"]["versions"], 1) + self.assertEqual([v["code"] for v in self.sent_versions()], ["DOCH-1"]) + + def test_version_text_missing_fails(self): # M-3: Eva не отдала text — не сохранять пустую версию + self.tree2(); self.eva.add_version("CmfDocument:1", "DOCH-1") + self.client.get_version = lambda code: {"code": code} + out = self.sync(known_versions={}) + self.assertEqual(out["status"], "failed") + self.assertEqual(out["error"], "версия DOCH-1 страницы DOC-1: Eva не отдала text (нет прав?)") + self.assertEqual(self.hub.versions, []) + def test_versions_batches_50(self): self.tree2() for i in range(120):