From 808d7b710cff9ee86c57eeec787f2f1bf9780e10 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Fri, 23 Oct 2026 21:39:25 +0400 Subject: [PATCH] =?UTF-8?q?test:=20=D1=8D=D1=82=D0=B0=D0=BB=D0=BE=D0=BD?= =?UTF-8?q?=D1=8B=20=D0=B2=D0=BD=D0=B5=D1=88=D0=BD=D0=B5=D0=B3=D0=BE=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=B2=D0=B5=D0=B4=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BF?= =?UTF-8?q?=D0=B5=D1=80=D0=B5=D0=B4=20=D0=BF=D0=B5=D1=80=D0=B5=D1=81=D1=82?= =?UTF-8?q?=D1=80=D0=BE=D0=B9=D0=BA=D0=BE=D0=B9=20(#87)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../fixtures/baseline/mcp_tools.expected.json | 500 ++++++++++++++++++ tests/fixtures/baseline/routes.expected.json | 206 ++++++++ tests/fixtures/baseline/schema.expected.json | 95 ++++ .../fixtures/baseline/settings.expected.json | 63 +++ tests/test_baseline.py | 44 ++ 5 files changed, 908 insertions(+) create mode 100644 tests/fixtures/baseline/mcp_tools.expected.json create mode 100644 tests/fixtures/baseline/routes.expected.json create mode 100644 tests/fixtures/baseline/schema.expected.json create mode 100644 tests/fixtures/baseline/settings.expected.json create mode 100644 tests/test_baseline.py diff --git a/tests/fixtures/baseline/mcp_tools.expected.json b/tests/fixtures/baseline/mcp_tools.expected.json new file mode 100644 index 0000000..cc7a33b --- /dev/null +++ b/tests/fixtures/baseline/mcp_tools.expected.json @@ -0,0 +1,500 @@ +[ + { + "description": "Задачи зеркала по фильтрам (спринт, исполнитель, вид работы, статус по факту, статус Jira, метка, эпик,\nподстрока темы/ключа, neighbor=1 — только соседние). Компактная таблица.", + "inputSchema": { + "properties": { + "assignee": { + "default": "", + "title": "Assignee", + "type": "string" + }, + "epic": { + "default": "", + "title": "Epic", + "type": "string" + }, + "fact_status": { + "default": "", + "title": "Fact Status", + "type": "string" + }, + "label": { + "default": "", + "title": "Label", + "type": "string" + }, + "limit": { + "default": 50, + "title": "Limit", + "type": "integer" + }, + "neighbor": { + "default": "", + "title": "Neighbor", + "type": "string" + }, + "q": { + "default": "", + "title": "Q", + "type": "string" + }, + "sprint": { + "default": "", + "title": "Sprint", + "type": "string" + }, + "status": { + "default": "", + "title": "Status", + "type": "string" + }, + "work_kind": { + "default": "", + "title": "Work Kind", + "type": "string" + } + }, + "title": "issuesArguments", + "type": "object" + }, + "name": "issues" + }, + { + "description": "Задача целиком: поля, связи в обе стороны, служебные заметки, последние команды, описание. Комментарии — 5\nпоследних с номером, текст до 300 символов; полный текст — инструмент comment по номеру. Вложения — списком\n(номер, имя, размер, где упомянуты, скачано ли); содержимое — инструмент attachment.", + "inputSchema": { + "properties": { + "key": { + "title": "Key", + "type": "string" + } + }, + "required": [ + "key" + ], + "title": "issueArguments", + "type": "object" + }, + "name": "issue" + }, + { + "description": "Один комментарий задачи целиком: автор, время, полный текст. comment_id — номер из ответа issue («#123»).", + "inputSchema": { + "properties": { + "comment_id": { + "title": "Comment Id", + "type": "string" + }, + "key": { + "title": "Key", + "type": "string" + } + }, + "required": [ + "key", + "comment_id" + ], + "title": "commentArguments", + "type": "object" + }, + "name": "comment" + }, + { + "description": "Скачать вложение задачи и вернуть путь к локальной копии (читайте файл по этому пути). attachment — номер из\nissue/comment («#31337» или «31337») либо точное имя файла; при нескольких одноимённых берётся самое новое.\nХаб просит агента скачать файл и сохраняет копию в downloads/ каталога данных; ожидание — до wait_seconds (1…600,\nпо умолчанию 120), не дождался — вызовите инструмент ещё раз: вторая команда не поставится. Файлы больше 10 МБ\nскачиваются только в Jira.", + "inputSchema": { + "properties": { + "attachment": { + "title": "Attachment", + "type": "string" + }, + "key": { + "title": "Key", + "type": "string" + }, + "wait_seconds": { + "default": 120, + "title": "Wait Seconds", + "type": "integer" + } + }, + "required": [ + "key", + "attachment" + ], + "title": "attachmentArguments", + "type": "object" + }, + "name": "attachment" + }, + { + "description": "Лента изменений задач: смена статуса, правка описания, сроки, новые комментарии. По умолчанию — с начала\nсегодняшнего дня; hours — за последние N часов; since/until — ISO-8601 (без смещения — местное время); kinds — через запятую из\nstatus, description, dates, comment, other или all; key — одна задача (без привязки к проекту).", + "inputSchema": { + "properties": { + "hours": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Hours" + }, + "key": { + "default": "", + "title": "Key", + "type": "string" + }, + "kinds": { + "default": "", + "title": "Kinds", + "type": "string" + }, + "since": { + "default": "", + "title": "Since", + "type": "string" + }, + "until": { + "default": "", + "title": "Until", + "type": "string" + } + }, + "title": "changesArguments", + "type": "object" + }, + "name": "changes" + }, + { + "description": "Правка служебных заметок задачи (только переданные поля).", + "inputSchema": { + "properties": { + "done_text": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Done Text" + }, + "fact_status": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Fact Status" + }, + "key": { + "title": "Key", + "type": "string" + }, + "owner_note": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Owner Note" + }, + "rest_text": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Rest Text" + }, + "work_kind": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Work Kind" + } + }, + "required": [ + "key" + ], + "title": "notes_setArguments", + "type": "object" + }, + "name": "notes_set" + }, + { + "description": "Поставить пакет команд. Каждая команда — {\"type\": <тип>, \"params\": {...}}. Хвостовой refresh хаб добавит сам.\nПараметры по типам (необязательные — в [ ]):\n- ensure_issue: temp_key (new:), issue_type (task|epic|bug), summary; [description, priority, duedate (YYYY-MM-DD), planned_start (YYYY-MM-DD), planned_end (YYYY-MM-DD), labels (список), epic_key (ключ или new:), assignee (логин Jira)]\n- ensure_fields: key; [summary, description, priority, duedate (YYYY-MM-DD), planned_start (YYYY-MM-DD), planned_end (YYYY-MM-DD), epic_key (ключ или new:), assignee (логин Jira), labels_add (список), labels_remove (список)] — хотя бы одно поле\n- ensure_comment: key, marker (метка в начале комментария), body\n- ensure_status: key, target (статус или список-путь)\n- ensure_link: source, target, link_type (логическое имя из link_types); [replace (true|false)]\n- ensure_sprint: key, sprint (имя спринта или backlog)\n- ensure_attachment: key, file (sha256 из POST /files; в MCP — path к локальному файлу), filename (имя вложения)\n- fetch_attachment: key, attachment_id (id вложения из issue (цифры))\n- refresh: —; [keys (список), jql] — keys или jql\nПроект: метку проекта сессии хаб дописывает в ensure_issue сам, метки других проектов запрещены, а команды над задачами вне проекта отклоняются; board_id для ensure_sprint выставляет хаб.\nensure_attachment из чата: вместо file передайте path — абсолютный путь к локальному файлу (до 10 МБ); MCP загрузит\nфайл в хаб и подставит file, filename по умолчанию — имя файла.", + "inputSchema": { + "properties": { + "commands": { + "items": { + "additionalProperties": true, + "type": "object" + }, + "title": "Commands", + "type": "array" + }, + "title": { + "title": "Title", + "type": "string" + } + }, + "required": [ + "title", + "commands" + ], + "title": "batch_createArguments", + "type": "object" + }, + "name": "batch_create" + }, + { + "description": "Состояние пакета и его команд.", + "inputSchema": { + "properties": { + "id": { + "title": "Id", + "type": "integer" + } + }, + "required": [ + "id" + ], + "title": "batch_statusArguments", + "type": "object" + }, + "name": "batch_status" + }, + { + "description": "Ждать завершения пакета (до 60 мин), вернуть итог по командам. Ожидание не блокирует event loop сервера.", + "inputSchema": { + "properties": { + "id": { + "title": "Id", + "type": "integer" + }, + "timeout_minutes": { + "default": 10, + "title": "Timeout Minutes", + "type": "integer" + } + }, + "required": [ + "id" + ], + "title": "batch_waitArguments", + "type": "object" + }, + "name": "batch_wait" + }, + { + "description": "Последние пакеты со статусом.", + "inputSchema": { + "properties": { + "limit": { + "default": 10, + "title": "Limit", + "type": "integer" + } + }, + "title": "batchesArguments", + "type": "object" + }, + "name": "batches" + }, + { + "description": "Повторить команду — новый пакет с теми же параметрами.", + "inputSchema": { + "properties": { + "id": { + "title": "Id", + "type": "integer" + } + }, + "required": [ + "id" + ], + "title": "command_retryArguments", + "type": "object" + }, + "name": "command_retry" + }, + { + "description": "Отменить пакет: команды в очереди отменяются; взятую агентом команду отмена не останавливает —\nв ответе подсказка (POST /commands/N/cancel с force).", + "inputSchema": { + "properties": { + "id": { + "title": "Id", + "type": "integer" + } + }, + "required": [ + "id" + ], + "title": "batch_cancelArguments", + "type": "object" + }, + "name": "batch_cancel" + }, + { + "description": "Обновить зеркало из Jira: по ключам или полностью (базовая JQL). По умолчанию только ставит пакет;\nwait=true — дождаться результата (до 30 мин).", + "inputSchema": { + "properties": { + "full": { + "default": false, + "title": "Full", + "type": "boolean" + }, + "keys": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Keys" + }, + "wait": { + "default": false, + "title": "Wait", + "type": "boolean" + } + }, + "title": "refreshArguments", + "type": "object" + }, + "name": "refresh" + }, + { + "description": "Агент: онлайн/офлайн, версия, текущая команда, 429 за сутки, команды без ответа, опубликованная версия,\nсостояние самообновления и запускатель.", + "inputSchema": { + "properties": {}, + "title": "agent_statusArguments", + "type": "object" + }, + "name": "agent_status" + }, + { + "description": "Журнал обновлений зеркала.", + "inputSchema": { + "properties": { + "limit": { + "default": 10, + "title": "Limit", + "type": "integer" + } + }, + "title": "sync_logArguments", + "type": "object" + }, + "name": "sync_log" + }, + { + "description": "Настройки хаба (все или одна по ключу).", + "inputSchema": { + "properties": { + "key": { + "default": "", + "title": "Key", + "type": "string" + } + }, + "title": "settings_getArguments", + "type": "object" + }, + "name": "settings_get" + }, + { + "description": "Записать настройку: value — JSON. Поднимает settings_version.", + "inputSchema": { + "properties": { + "key": { + "title": "Key", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "key", + "value" + ], + "title": "settings_setArguments", + "type": "object" + }, + "name": "settings_set" + }, + { + "description": "Проекты хаба: метка, название, доска, тип доски, число задач; текущий проект сессии отмечен.", + "inputSchema": { + "properties": {}, + "title": "projectsArguments", + "type": "object" + }, + "name": "projects" + }, + { + "description": "Завести проект (метка Jira, название, необязательно id доски). Хаб ставит пакет обновления; в ответе — фрагмент\n.mcp.json для репозитория проекта.", + "inputSchema": { + "properties": { + "board_id": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Board Id" + }, + "label": { + "title": "Label", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + } + }, + "required": [ + "label", + "name" + ], + "title": "project_createArguments", + "type": "object" + }, + "name": "project_create" + } +] diff --git a/tests/fixtures/baseline/routes.expected.json b/tests/fixtures/baseline/routes.expected.json new file mode 100644 index 0000000..9f58022 --- /dev/null +++ b/tests/fixtures/baseline/routes.expected.json @@ -0,0 +1,206 @@ +[ + [ + "GET", + "^/(links)?$", + null, + "h_root" + ], + [ + "GET", + "^/agent$", + "client", + "h_agent_get" + ], + [ + "POST", + "^/agent/claim$", + "agent", + "h_claim" + ], + [ + "GET", + "^/agent/commands/(\\d+)$", + "agent", + "h_agent_command" + ], + [ + "POST", + "^/agent/commands/(\\d+)/result$", + "agent", + "h_agent_result" + ], + [ + "POST", + "^/agent/files$", + "agent", + "h_agent_files_post" + ], + [ + "GET", + "^/agent/files/([^/]+)$", + "agent", + "h_agent_file" + ], + [ + "POST", + "^/agent/history$", + "agent", + "h_agent_history" + ], + [ + "GET", + "^/agent/known-keys$", + "agent", + "h_known_keys" + ], + [ + "GET", + "^/agent/release$", + "agent", + "h_agent_release" + ], + [ + "GET", + "^/agent/settings$", + "agent", + "h_agent_settings" + ], + [ + "POST", + "^/agent/state$", + "agent", + "h_agent_state" + ], + [ + "GET", + "^/app(?:/(.*))?$", + null, + "h_app" + ], + [ + "GET", + "^/attachments/(\\d+)$", + "client", + "h_attachment_get" + ], + [ + "GET", + "^/attachments/(\\d+)/content$", + "client", + "h_attachment_content" + ], + [ + "POST", + "^/attachments/(\\d+)/fetch$", + "client", + "h_attachment_fetch" + ], + [ + "GET", + "^/batches$", + "client", + "h_batches_get" + ], + [ + "POST", + "^/batches$", + "client", + "h_batches_post" + ], + [ + "GET", + "^/batches/(\\d+)$", + "client", + "h_batch_get" + ], + [ + "POST", + "^/batches/(\\d+)/cancel$", + "client", + "h_batch_cancel" + ], + [ + "GET", + "^/board$", + "client", + "h_board" + ], + [ + "GET", + "^/changes$", + "client", + "h_changes" + ], + [ + "POST", + "^/commands/(\\d+)/cancel$", + "client", + "h_command_cancel" + ], + [ + "POST", + "^/commands/(\\d+)/retry$", + "client", + "h_command_retry" + ], + [ + "POST", + "^/files$", + "client", + "h_files_post" + ], + [ + "GET", + "^/issues$", + "client", + "h_issues" + ], + [ + "GET", + "^/issues/([^/]+)$", + "client", + "h_issue" + ], + [ + "PUT", + "^/issues/([^/]+)/note$", + "client", + "h_note_put" + ], + [ + "GET", + "^/projects$", + "client", + "h_projects_get" + ], + [ + "POST", + "^/projects$", + "client", + "h_projects_post" + ], + [ + "POST", + "^/refresh$", + "client", + "h_refresh" + ], + [ + "GET", + "^/settings$", + "client", + "h_settings_get" + ], + [ + "PUT", + "^/settings$", + "client", + "h_settings_put" + ], + [ + "GET", + "^/sync-runs$", + "client", + "h_sync_runs" + ] +] diff --git a/tests/fixtures/baseline/schema.expected.json b/tests/fixtures/baseline/schema.expected.json new file mode 100644 index 0000000..d25a3a5 --- /dev/null +++ b/tests/fixtures/baseline/schema.expected.json @@ -0,0 +1,95 @@ +{ + "objects": [ + [ + "index", + "issue_attachment_key", + "CREATE INDEX issue_attachment_key ON issue_attachment(key)" + ], + [ + "index", + "ix_command_status", + "CREATE INDEX ix_command_status ON command(status, batch_id, seq)" + ], + [ + "index", + "ix_issue_change_at", + "CREATE INDEX ix_issue_change_at ON issue_change(at)" + ], + [ + "index", + "ix_issue_change_key", + "CREATE INDEX ix_issue_change_key ON issue_change(key, at)" + ], + [ + "index", + "ix_issue_link_target", + "CREATE INDEX ix_issue_link_target ON issue_link(target_key)" + ], + [ + "table", + "agent_status", + "CREATE TABLE agent_status (\n id INTEGER PRIMARY KEY CHECK (id = 1),\n last_seen_at TEXT,\n agent_version TEXT,\n settings_version INTEGER,\n current_command_id INTEGER,\n last_jira_call_at TEXT\n)" + ], + [ + "table", + "attachment_file", + "CREATE TABLE attachment_file (\n attachment_id TEXT PRIMARY KEY,\n file TEXT NOT NULL,\n size INTEGER NOT NULL,\n fetched_at TEXT NOT NULL\n)" + ], + [ + "table", + "batch", + "CREATE TABLE batch (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n title TEXT NOT NULL,\n author TEXT NOT NULL,\n created_at TEXT NOT NULL,\n cancelled_at TEXT,\n estimate_seconds INTEGER NOT NULL DEFAULT 0,\n project TEXT\n)" + ], + [ + "table", + "command", + "CREATE TABLE command (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n batch_id INTEGER NOT NULL REFERENCES batch(id),\n seq INTEGER NOT NULL,\n type TEXT NOT NULL,\n params TEXT NOT NULL,\n status TEXT NOT NULL DEFAULT 'queued',\n claimed_at TEXT,\n deliveries INTEGER NOT NULL DEFAULT 0,\n finished_at TEXT,\n result TEXT,\n error TEXT,\n jira_calls INTEGER NOT NULL DEFAULT 0,\n rate_limited INTEGER NOT NULL DEFAULT 0,\n pause_seconds INTEGER,\n depends_on INTEGER REFERENCES command(id)\n)" + ], + [ + "table", + "issue", + "CREATE TABLE issue (\n key TEXT PRIMARY KEY,\n id TEXT,\n issue_type TEXT,\n summary TEXT,\n description TEXT,\n status TEXT,\n priority TEXT,\n assignee TEXT,\n reporter TEXT,\n duedate TEXT,\n planned_start TEXT,\n planned_end TEXT,\n epic_key TEXT,\n labels TEXT NOT NULL DEFAULT '[]',\n sprint TEXT,\n created TEXT,\n jira_updated TEXT,\n fetched_at TEXT NOT NULL,\n neighbor INTEGER NOT NULL DEFAULT 0\n)" + ], + [ + "table", + "issue_attachment", + "CREATE TABLE issue_attachment (\n attachment_id TEXT PRIMARY KEY,\n key TEXT NOT NULL,\n filename TEXT NOT NULL,\n size INTEGER,\n mime_type TEXT,\n author TEXT,\n created TEXT,\n created_at TEXT\n)" + ], + [ + "table", + "issue_change", + "CREATE TABLE issue_change (\n history_id TEXT NOT NULL,\n seq INTEGER NOT NULL,\n key TEXT NOT NULL,\n at TEXT,\n created TEXT NOT NULL,\n author TEXT,\n field TEXT NOT NULL,\n field_id TEXT,\n field_name TEXT NOT NULL,\n from_value TEXT,\n from_string TEXT,\n to_value TEXT,\n to_string TEXT,\n fetched_at TEXT NOT NULL,\n PRIMARY KEY (history_id, seq)\n)" + ], + [ + "table", + "issue_comment", + "CREATE TABLE issue_comment (\n key TEXT NOT NULL,\n comment_id TEXT NOT NULL,\n author TEXT,\n created TEXT,\n updated TEXT,\n body TEXT NOT NULL DEFAULT '',\n created_at TEXT,\n PRIMARY KEY (key, comment_id)\n)" + ], + [ + "table", + "issue_link", + "CREATE TABLE issue_link (\n source_key TEXT NOT NULL,\n target_key TEXT NOT NULL,\n link_type TEXT NOT NULL,\n PRIMARY KEY (source_key, target_key, link_type)\n)" + ], + [ + "table", + "issue_note", + "CREATE TABLE issue_note (\n key TEXT PRIMARY KEY,\n fact_status TEXT,\n work_kind TEXT,\n done_text TEXT,\n rest_text TEXT,\n owner_note TEXT,\n updated_at TEXT,\n updated_by TEXT\n)" + ], + [ + "table", + "project", + "CREATE TABLE project (\n label TEXT PRIMARY KEY,\n name TEXT NOT NULL,\n board_id INTEGER,\n board_type TEXT,\n created_at TEXT NOT NULL\n)" + ], + [ + "table", + "setting", + "CREATE TABLE setting (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL\n)" + ], + [ + "table", + "sync_run", + "CREATE TABLE sync_run (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n started_at TEXT NOT NULL,\n finished_at TEXT,\n kind TEXT NOT NULL,\n query TEXT,\n issues_count INTEGER NOT NULL DEFAULT 0,\n links_count INTEGER NOT NULL DEFAULT 0,\n jira_calls INTEGER NOT NULL DEFAULT 0,\n rate_limited INTEGER NOT NULL DEFAULT 0,\n command_id INTEGER,\n history_at TEXT\n)" + ] + ], + "schema_version": 6 +} diff --git a/tests/fixtures/baseline/settings.expected.json b/tests/fixtures/baseline/settings.expected.json new file mode 100644 index 0000000..dcfe1d1 --- /dev/null +++ b/tests/fixtures/baseline/settings.expected.json @@ -0,0 +1,63 @@ +{ + "agent_keys": [ + "project_key", + "issue_types", + "epic_name_field_id", + "field_ids", + "epic_link", + "priority_map", + "priority_value_key", + "labels", + "status_labels", + "kind_labels", + "comment_marker", + "transitions_map", + "link_types", + "board_id", + "pacing", + "min_agent_version", + "settings_version" + ], + "defaults": { + "auto_refresh_minutes": 60, + "backup_keep": 30, + "closed_statuses": [ + "Готово", + "Снято" + ], + "comment_marker": "", + "epic_link": {}, + "epic_name_field_id": null, + "field_ids": {}, + "initialized": false, + "issue_types": {}, + "kind_labels": {}, + "labels": [], + "link_types": {}, + "min_agent_version": 1, + "pacing": { + "ensure_attachment": 30, + "ensure_comment": 30, + "ensure_fields": 120, + "ensure_issue": 120, + "ensure_link": 30, + "ensure_sprint": 30, + "ensure_status": 30, + "fetch_attachment": 2, + "refresh": 2 + }, + "port": 8790, + "priority_map": {}, + "priority_value_key": "name", + "project_key": "AURORA", + "relay_dir": null, + "settings_version": 1, + "status_labels": {}, + "transitions_map": {} + }, + "protected": [ + "schema_version", + "settings_version", + "tokens" + ] +} diff --git a/tests/test_baseline.py b/tests/test_baseline.py new file mode 100644 index 0000000..6df8183 --- /dev/null +++ b/tests/test_baseline.py @@ -0,0 +1,44 @@ +"""Эталоны внешнего поведения (golden): MCP-инструменты, маршруты API, настройки, схема базы. +Перестройка на модули обязана их сохранить. Запись эталонов: BASELINE_WRITE=1 uv run pytest tests/test_baseline.py.""" +import asyncio +import json +import os +from pathlib import Path + +from jira_hub import api, db, mcp_server, settings + +FIXTURES = Path(__file__).parent / "fixtures" / "baseline" + + +def _check(name, snapshot): + path = FIXTURES / f"{name}.expected.json" + text = json.dumps(snapshot, ensure_ascii=False, indent=2, sort_keys=True) + "\n" + if os.environ.get("BASELINE_WRITE") == "1": + FIXTURES.mkdir(parents=True, exist_ok=True) + path.write_text(text, encoding="utf-8") + assert path.read_text(encoding="utf-8") == text + + +def test_mcp_tools(): + tools = asyncio.run(mcp_server.mcp.list_tools()) + _check("mcp_tools", [{"name": t.name, "description": t.description, "inputSchema": t.inputSchema} + for t in tools]) + + +def test_routes(): + rows = [[method, pattern, scope, fn.__name__] for method, pattern, fn, scope in api.ROUTES] + _check("routes", sorted(rows, key=lambda r: (r[1], r[0]))) + + +def test_settings(): + _check("settings", {"defaults": settings.DEFAULTS, "agent_keys": settings.AGENT_KEYS, + "protected": sorted(settings.PROTECTED)}) + + +def test_schema(tmp_path): + conn = db.connect(tmp_path / "hub.sqlite") + rows = conn.execute("SELECT type, name, sql FROM sqlite_master WHERE name NOT LIKE 'sqlite_%' " + "ORDER BY type, name").fetchall() + version = db._schema_version(conn) + conn.close() + _check("schema", {"schema_version": version, "objects": [[r[0], r[1], r[2]] for r in rows]})