Модуль wiki, агенты модулей, поля проектов (#91) #92
@ -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)
|
||||
|
||||
@ -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]}}}
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user