# Один запуск проверки лаборатории: успехи, отказы и завершение процессов

После изменения инструмента легко проверить один счастливый вызов и забыть схему или отказ. Соберём пять уже работающих проверок в последовательный запуск, который останавливается на первом падении и возвращает ненулевой код. Результат пригоден для локальной проверки перед изменением конфигурации клиента; внешние API и модель ему не нужны.

## Перед началом

Пройдите предыдущие пять шагов маршрута. В одной папке должны лежать три сервера 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`:

```python
"""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
```

Ожидаемый вывод:

```text
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_stdio | initialize, схема, Draft и три отказа первого сервера |
| inspect_contract | контракт SDK-клиента, внутренние пробелы, неизвестный tool |
| check_validation | необязательный integer/default, два успеха, четыре отказа |
| check_context | ресурс, шаблон сообщения, два протокольных исключения |
| check_transport | настоящий stdio и чистая логика конфигурации HTTP |

Чтение этих результатов не заменяет проверку интерфейса редактора. Проверка HTTP здесь не обращается в сеть и не утверждает, что создан удалённый сервер. Ожидаемые отказы — часть успеха теста: инструмент должен отвергать плохие данные, а не всегда возвращать объект label.

## 3. Проверяйте функцию и протокол раздельно

Сохраните `test_lab.py`. Здесь стандартный unittest проверяет прямую функцию на таблицах входов, всю stdio-suite и намеренно испорченное ожидание в отдельной временной копии. Временная папка принадлежит тесту и удаляется его контекстом. Исходные серверы и проверки не перезаписываются:

```python
"""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
```

Ожидаются пять тестов с 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](https://py.sdk.modelcontextprotocol.io/v1/testing/), [tools 2025-11-25](https://modelcontextprotocol.io/specification/2025-11-25/server/tools), [клиент SDK v1](https://py.sdk.modelcontextprotocol.io/v1/client/), [HTTP-авторизация](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization).
