Проверка≈ 30 мин с практикой

Один запуск проверки лаборатории: успехи, отказы и завершение процессов

После изменения инструмента легко проверить один счастливый вызов и забыть схему или отказ. Соберём пять уже работающих проверок в последовательный запуск, который останавливается на первом падении и возвращает ненулевой код. Результат пригоден для локальной проверки перед изменением конфигурации клиента; внешние API и модель ему не нужны.

В результате

Последовательная suite, тесты функций и реального stdio, обнаружение намеренного падения.

Перед началом

Все пять предыдущих шагов одной лаборатории.

Скачать руководство целиком · Markdown
Граница версии

Python SDK v1 · mcp==1.30.0 · протокол 2025-11-25. Для SDK v2 требуется отдельная миграция.

Перед началом

Пройдите предыдущие пять шагов маршрута. В одной папке должны лежать три сервера label_server.py, validation_server.py, context_server.py; общий lab_session.py; check_stdio.py, inspect_contract.py, check_validation.py, check_context.py, check_transport.py; configure_client.py и созданная .mcp.json. Используется .venv из первого урока с mcp 1.30.0. Если чего-то нет, скачайте полный файл из соответствующего урока, а не дописывайте отсутствующие функции по догадке.

1. Сохраните последовательный запуск

Создайте check_suite.py:

check_suite.pyPython
Скачать
"""Run the lab's independent checks sequentially and stop on a failure."""
from pathlib import Path
import subprocess
import sys

root = Path(__file__).resolve().parent
checks = ["check_stdio.py", "inspect_contract.py", "check_validation.py", "check_context.py", "check_transport.py"]
for filename in checks:
    result = subprocess.run([sys.executable, str(root / filename)], cwd=root, capture_output=True, text=True, encoding="utf-8", timeout=30)
    if result.returncode:
        print("FAIL:", filename)
        print(result.stdout)
        print(result.stderr, file=sys.stderr)
        raise SystemExit(result.returncode)
    print("PASS:", filename)
print("PASS: all five lab checks")
check_suite.pyPython
"""Run the lab's independent checks sequentially and stop on a failure."""
from pathlib import Path
import subprocess
import sys

root = Path(__file__).resolve().parent
checks = ["check_stdio.py", "inspect_contract.py", "check_validation.py", "check_context.py", "check_transport.py"]
for filename in checks:
    result = subprocess.run([sys.executable, str(root / filename)], cwd=root, capture_output=True, text=True, encoding="utf-8", timeout=30)
    if result.returncode:
        print("FAIL:", filename)
        print(result.stdout)
        print(result.stderr, file=sys.stderr)
        raise SystemExit(result.returncode)
    print("PASS:", filename)
print("PASS: all five lab checks")

Каждая проверка запускается через тот же sys.executable, что и suite. Это защищает от случайного Python из PATH с другой версией SDK. У дочерней проверки есть предел 30 секунд. При ненулевом коде показываются её stdout и stderr, затем suite возвращает этот код. Это не тест одного большого постоянно работающего сервера: у каждой проверки собственный короткий жизненный цикл.

2. Проверьте все слои

PowerShell
.\.venv\Scripts\python.exe check_suite.py
$LASTEXITCODE
PowerShellPowerShell
.\.venv\Scripts\python.exe check_suite.py
$LASTEXITCODE

Ожидаемый вывод:

Ожидаемый вывод
PASS: check_stdio.py
PASS: inspect_contract.py
PASS: check_validation.py
PASS: check_context.py
PASS: check_transport.py
PASS: all five lab checks
0
Ожидаемый вывод
PASS: check_stdio.py
PASS: inspect_contract.py
PASS: check_validation.py
PASS: check_context.py
PASS: check_transport.py
PASS: all five lab checks
0
ПроверкаКакой результат она подтверждает
check_stdioinitialize, схема, Draft и три отказа первого сервера
inspect_contractконтракт SDK-клиента, внутренние пробелы, неизвестный tool
check_validationнеобязательный integer/default, два успеха, четыре отказа
check_contextресурс, шаблон сообщения, два протокольных исключения
check_transportнастоящий stdio и чистая логика конфигурации HTTP

Чтение этих результатов не заменяет проверку интерфейса редактора. Проверка HTTP здесь не обращается в сеть и не утверждает, что создан удалённый сервер. Ожидаемые отказы — часть успеха теста: инструмент должен отвергать плохие данные, а не всегда возвращать объект label.

3. Проверяйте функцию и протокол раздельно

Сохраните test_lab.py. Здесь стандартный unittest проверяет прямую функцию на таблицах входов, всю stdio-suite и намеренно испорченное ожидание в отдельной временной копии. Временная папка принадлежит тесту и удаляется его контекстом. Исходные серверы и проверки не перезаписываются:

test_lab.pyPython
Скачать
"""Direct fixtures, a real stdio suite, and a failing isolated expectation."""
from pathlib import Path
import shutil
import subprocess
import sys
import tempfile
import unittest
from validation_server import normalize_label

ROOT = Path(__file__).resolve().parent


class FunctionTests(unittest.TestCase):
    def test_success_fixtures(self):
        cases = [("  Draft  ", 5, {"label": "Draft"}), (" two  words ", 80, {"label": "two  words"}), ("x", 1, {"label": "x"}), ("x" * 80, 80, {"label": "x" * 80})]
        for text, limit, expected in cases:
            with self.subTest(text=text, limit=limit):
                self.assertEqual(normalize_label(text, limit), expected)

    def test_invalid_text_fixtures(self):
        for text, limit in [("   ", 80), ("Draft!", 5), ("xx", 1)]:
            with self.subTest(text=text, limit=limit):
                with self.assertRaisesRegex(ValueError, "after trimming"):
                    normalize_label(text, limit)

    def test_invalid_limit_fixtures(self):
        for limit in [0, -1, 81]:
            with self.subTest(limit=limit):
                with self.assertRaisesRegex(ValueError, "between 1 and 80"):
                    normalize_label("Draft", limit)


class ProtocolTests(unittest.TestCase):
    def test_all_stdio_checks(self):
        result = subprocess.run([sys.executable, str(ROOT / "check_suite.py")], cwd=ROOT, capture_output=True, text=True, encoding="utf-8", timeout=150)
        self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
        self.assertIn("PASS: all five lab checks", result.stdout)

    def test_wrong_expectation_really_fails(self):
        with tempfile.TemporaryDirectory(prefix="label-lab-negative-") as temporary:
            work = Path(temporary)
            for name in ["label_server.py", "lab_session.py", "check_suite.py", "check_stdio.py"]:
                shutil.copyfile(ROOT / name, work / name)
            original = (ROOT / "inspect_contract.py").read_text(encoding="utf-8")
            expectation = 'assert result.structuredContent == {"label": "two  words"}'
            self.assertEqual(original.count(expectation), 1)
            wrong = original.replace(expectation, 'assert result.structuredContent == {"label": "two words"}', 1)
            (work / "inspect_contract.py").write_text(wrong, encoding="utf-8")
            result = subprocess.run([sys.executable, str(work / "check_suite.py")], cwd=work, capture_output=True, text=True, encoding="utf-8", timeout=60)
            self.assertNotEqual(result.returncode, 0)
            self.assertIn("PASS: check_stdio.py", result.stdout)
            self.assertIn("FAIL: inspect_contract.py", result.stdout)
            self.assertIn("AssertionError", result.stderr)
            self.assertNotIn("PASS: check_validation.py", result.stdout)


if __name__ == "__main__":
    unittest.main(verbosity=2)
test_lab.pyPython
"""Direct fixtures, a real stdio suite, and a failing isolated expectation."""
from pathlib import Path
import shutil
import subprocess
import sys
import tempfile
import unittest
from validation_server import normalize_label

ROOT = Path(__file__).resolve().parent


class FunctionTests(unittest.TestCase):
    def test_success_fixtures(self):
        cases = [("  Draft  ", 5, {"label": "Draft"}), (" two  words ", 80, {"label": "two  words"}), ("x", 1, {"label": "x"}), ("x" * 80, 80, {"label": "x" * 80})]
        for text, limit, expected in cases:
            with self.subTest(text=text, limit=limit):
                self.assertEqual(normalize_label(text, limit), expected)

    def test_invalid_text_fixtures(self):
        for text, limit in [("   ", 80), ("Draft!", 5), ("xx", 1)]:
            with self.subTest(text=text, limit=limit):
                with self.assertRaisesRegex(ValueError, "after trimming"):
                    normalize_label(text, limit)

    def test_invalid_limit_fixtures(self):
        for limit in [0, -1, 81]:
            with self.subTest(limit=limit):
                with self.assertRaisesRegex(ValueError, "between 1 and 80"):
                    normalize_label("Draft", limit)


class ProtocolTests(unittest.TestCase):
    def test_all_stdio_checks(self):
        result = subprocess.run([sys.executable, str(ROOT / "check_suite.py")], cwd=ROOT, capture_output=True, text=True, encoding="utf-8", timeout=150)
        self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
        self.assertIn("PASS: all five lab checks", result.stdout)

    def test_wrong_expectation_really_fails(self):
        with tempfile.TemporaryDirectory(prefix="label-lab-negative-") as temporary:
            work = Path(temporary)
            for name in ["label_server.py", "lab_session.py", "check_suite.py", "check_stdio.py"]:
                shutil.copyfile(ROOT / name, work / name)
            original = (ROOT / "inspect_contract.py").read_text(encoding="utf-8")
            expectation = 'assert result.structuredContent == {"label": "two  words"}'
            self.assertEqual(original.count(expectation), 1)
            wrong = original.replace(expectation, 'assert result.structuredContent == {"label": "two words"}', 1)
            (work / "inspect_contract.py").write_text(wrong, encoding="utf-8")
            result = subprocess.run([sys.executable, str(work / "check_suite.py")], cwd=work, capture_output=True, text=True, encoding="utf-8", timeout=60)
            self.assertNotEqual(result.returncode, 0)
            self.assertIn("PASS: check_stdio.py", result.stdout)
            self.assertIn("FAIL: inspect_contract.py", result.stdout)
            self.assertIn("AssertionError", result.stderr)
            self.assertNotIn("PASS: check_validation.py", result.stdout)


if __name__ == "__main__":
    unittest.main(verbosity=2)

Выполните:

PowerShell
.\.venv\Scripts\python.exe -m unittest -v test_lab
$LASTEXITCODE
PowerShellPowerShell
.\.venv\Scripts\python.exe -m unittest -v test_lab
$LASTEXITCODE

Ожидаются пять тестов с ok, строка Ran 5 tests, OK и код 0. test_success_fixtures проверяет крайние и внутренние пробелы, минимальную и максимальную длину. Два следующих теста проверяют ошибки прямой функции. ProtocolTests запускает настоящий обмен и проверяет, что изменение ожидания вызывает падение и останавливает suite. Прямая функция не подтверждает схему или MCP-регистрацию; протокольный тест не заменяет таблицу предметных правил. Оба уровня нужны по своим причинам.

4. Убедитесь, что suite замечает падение

Сделайте отдельную копию лаборатории для опыта. В копии inspect_contract.py замените ожидаемый результат two words на two words, сохранив вызов с двойным внутренним пробелом. Запустите suite. Он должен напечатать PASS только для первого файла, затем FAIL: inspect_contract.py, traceback утверждения и ненулевой $LASTEXITCODE. Остальные три проверки не должны выполняться после этого падения.

Верните точное ожидаемое значение и повторите запуск: все пять должны снова пройти. Не проводите этот опыт в исходном готовом проекте без копии. Смысл опыта — проверить поведение управляющего скрипта, а не ослабить проверку инструмента.

Как разбирать ошибку

При FAIL сначала запустите отдельно указанный файл тем же интерпретатором. ModuleNotFoundError означает проблему среды или отсутствующий общий файл. AssertionError схемы означает изменение контракта. TimeoutExpired означает превышение предела проверки; изучите зависший шаг и stdout/stderr, а не просто увеличивайте таймаут. Если сервер печатает обычные строки в stdout, восстановите разделение протокола и диагностики.

Данные ответа и ошибки проверяются независимо. Для tools/call нужно рассматривать isError; для чтения ресурса или получения prompt — исключения McpError. Полный stderr при отрицательных сценариях может содержать ожидаемые диагностические записи сервера. Пустой stderr сам по себе не является доказательством исправности.

Что проверять при подключении внешнего API

Следующий отдельный этап — изолированный клиент API с синтетическими ответами: успех, неверный вход, 4xx, 5xx, timeout, ограничение частоты и повторные запросы. Здесь такого клиента нет. Также нет операций записи и доступа к персональным данным. Для удалённого HTTP-сервера нужны отдельные проверки авторизации и отказа доступа; наша локальная suite не подтверждает эти свойства.

Проверьте себя

Составьте таблицу «изменение → ожидаемое падение». Например, переименование tool должно падать на tools/list, изменение strip — на structuredContent, удаление prompt — на prompts/list. Если ни один тест не замечает важное изменение, добавьте проверку поведения до расширения возможностей сервера. Сохранённый first_tool.py из дополнительного разбора остаётся отдельным примером и не включён в эту лабораторию автоматически.

Версия и источники

Упражнение закреплено за Python 3.11 и mcp==1.30.0, SDK v1, протоколом 2025-11-25. SDK v2 требует отдельной миграции. Примеры используют синтетические подписи, без внешнего API и модели. Документация сверена 8 октября 2026 года.

Пять сценариев запускаются последовательно на SDK 1.30.0. Проверки среды используют уже установленный пакет; установка в новой пользовательской среде и интерфейс редактора — отдельные шаги первого урока. HTTP и права доступа не выдаются за проверенные.

Источники: testing SDK v1 (в новой вкладке), tools 2025-11-25 (в новой вкладке), клиент SDK v1 (в новой вкладке), HTTP-авторизация (в новой вкладке).

Вопросы по материалу

Почему после одной ошибки остальные проверки не запускаются?

Так устроен check_suite.py: при первом ненулевом коде он печатает вывод упавшей проверки и завершает запуск. Начните с указанного файла в том же окружении, затем повторите всю последовательность.

Если тесты функции прошли, зачем ещё проверять stdio?

Прямой вызов проверяет правила обработки подписи. Он не проверяет регистрацию инструмента, опубликованную схему и обмен MCP. Для этих условий лаборатория запускает отдельные протокольные проверки.

Продукты ATOMNO

Другие задачи для AI-помощника

Это отдельные продукты ATOMNO для задач с данными. Они не входят в учебные примеры.

Все продукты ATOMNO (в новой вкладке)