Модуль wiki, агенты модулей, поля проектов (#91) #92

Merged
aep merged 46 commits from feat/91-wiki into main 2026-10-24 09:05:09 +03:00
6 changed files with 41 additions and 10 deletions
Showing only changes of commit 82bbf0f5cd - Show all commits

View File

@ -39,6 +39,7 @@ def main(argv=None) -> int:
return 2
try:
secure_dir(paths.release_root())
release.migrate_layout(paths.release_root()) # прежняя раскладка release/ до проверки «только вверх»
m = release.publish(release.module_dir(paths.release_root(), args.module), args.path)
except release.PublishError as e:
print(e, file=sys.stderr)

View File

@ -25,11 +25,11 @@ def unknown_project(label) -> str:
return f"проект {label} не заведён в хабе: project_create или исправьте --project в .mcp.json"
def mcp_json(label: str, module: str = "jira") -> dict:
"""Фрагмент .mcp.json для репозитория проекта: у jira прежний (сервер jira-hub), у прочих модулей — сервер
jira-hub-<модуль>."""
def mcp_json(label: str, module: str = registry.DEFAULT_AGENT) -> dict:
"""Фрагмент .mcp.json для репозитория проекта: у модуля по умолчанию прежний (сервер jira-hub), у прочих модулей —
сервер jira-hub-<модуль>."""
run = ["run", "--project", str(HUB_ROOT), "jira-hub", "mcp"]
if module == "jira":
if module == registry.DEFAULT_AGENT:
return {"mcpServers": {"jira-hub": {"command": "uv", "args": [*run, "--project", label]}}}
return {"mcpServers": {f"jira-hub-{module}": {"command": "uv", "args": [*run, module, "--project", label]}}}

View File

@ -19,7 +19,7 @@ VERSION_RE = re.compile(r"^AGENT_VERSION = (\d+)[ \t]*\r?$", re.M)
SHA256_RE = re.compile(r"[0-9a-f]{64}")
_warned: str | None = None # текст последнего предупреждения о манифесте: одно на каждую новую ошибку (решение 5)
_warned: dict[str, str] = {} # путь манифеста → текст последнего предупреждения: одно на каждый новый текст на путь (решение 5)
_warned_lock = threading.Lock()
@ -152,12 +152,14 @@ def _manifest_problem(m) -> str | None:
def _note_manifest(problem: str | None, path: Path) -> None:
"""Предупреждение о негодном манифесте — один раз на каждый новый текст; годный или отсутствующий манифест
сбрасывает память (решение 5). Замок: claim и GET /agent читают манифест из разных потоков."""
global _warned
text = None if problem is None else f"release: манифест {path}: {problem} — публикации нет"
key = str(path)
with _warned_lock:
if text is not None and text != _warned:
if text is None:
_warned.pop(key, None)
elif _warned.get(key) != text:
log.warning("%s", text)
_warned = text
_warned[key] = text
def current(release_dir) -> dict | None:

View File

@ -498,7 +498,7 @@ def test_claim_survives_unreadable_release(hub):
def test_invalid_manifest_one_warning_for_many_claims(hub, monkeypatch, caplog):
"""M-3/M-7 #25, спека «Негодный манифест»: version = 6.9 — публикации нет; 10 claim и GET /agent — одно предупреждение."""
monkeypatch.setattr(release, "_warned", None)
monkeypatch.setattr(release, "_warned", {})
rd = release.module_dir(hub["hub"].release_root, "jira")
(rd / "manifest.json").write_text('{"version": 6.9, "sha256": "' + "a" * 64 + '"}', encoding="utf-8")
for _ in range(10):

View File

@ -89,3 +89,18 @@ def test_mcp_unknown_module(monkeypatch, capsys):
err = capsys.readouterr().err
assert "неизвестный модуль nope" in err and "jira" in err
assert seen == []
def test_agent_publish_migrates_old_layout(tmp_path, monkeypatch, capsys):
"""M-3: корневые agent.py/manifest.json прежней раскладки переезжают в release/jira/ до проверки «только вверх»."""
monkeypatch.setenv("JIRA_HUB_HOME", str(tmp_path))
rel = tmp_path / "release"
rel.mkdir()
old = tmp_path / "old.py"
old.write_text("AGENT_VERSION = 14\n", encoding="utf-8")
release.publish(rel, old) # прежняя раскладка: release/agent.py + manifest.json
new = tmp_path / "new.py"
new.write_text("AGENT_VERSION = 13\n", encoding="utf-8")
assert cli.main(["agent", "publish", str(new)]) == 1 # v13 < v14: «только вверх» видит прежнюю публикацию
assert not (rel / "agent.py").exists() and not (rel / "manifest.json").exists()
assert json.loads((rel / "jira" / "manifest.json").read_text(encoding="utf-8"))["version"] == 14

View File

@ -223,7 +223,7 @@ def test_invalid_manifest_means_no_release(tmp_path, manifest):
def test_invalid_manifest_warns_once_per_new_error(tmp_path, monkeypatch, caplog):
"""M-3 #25, решение 5: одно предупреждение на каждый новый текст ошибки; годный манифест сбрасывает память."""
monkeypatch.setattr(release, "_warned", None)
monkeypatch.setattr(release, "_warned", {})
rd = tmp_path / "release"
rd.mkdir()
manifest = rd / "manifest.json"
@ -285,3 +285,16 @@ def test_hint_unknown_launcher_until_reported():
assert release.hint(rel, online9, None, None, reported=True) == "агент запущен без run.py — обновится только вручную"
rb = {"state": "rolled_back", "from_version": 9, "to_version": 10, "error": "код 1"}
assert release.hint(rel, online9, rb, None, reported=False) == "откат с v10 на v9: код 1"
def test_invalid_manifest_warning_not_repeated_across_alternating_dirs(tmp_path, monkeypatch, caplog):
"""M-1: два агента — негодный манифест одного и отсутствующий у другого не сбрасывают память друг друга."""
monkeypatch.setattr(release, "_warned", {})
bad, none = tmp_path / "jira", tmp_path / "wiki"
bad.mkdir()
none.mkdir()
(bad / "manifest.json").write_text('{"version": 0, "sha256": "x"}', encoding="utf-8")
for _ in range(10):
assert release.current(bad) is None
assert release.current(none) is None
assert len([r for r in caplog.records if "манифест" in r.getMessage()]) == 1