fix(doctor): check the config file the Gateway actually loads (#5987)

* fix(doctor): check the config file the Gateway actually loads

make doctor always inspected <checkout>/config.yaml and ignored
DEER_FLOW_CONFIG_PATH and DEER_FLOW_PROJECT_ROOT. A missing override
that stops the Gateway from starting still reported "config.yaml found"
and "config.yaml loadable", and a valid override pointing elsewhere got
the wrong file checked.

Doctor now resolves the path through the harness's own resolver,
AppConfig.resolve_config_path, after defaulting DEER_FLOW_PROJECT_ROOT
to the checkout as scripts/serve.sh does for make dev. An override the
Gateway would reject fails "config.yaml found" with the Gateway's error
and the config checks skip; an unimportable harness is reported rather
than raised.

* docs(changelog): reference #5987

* fix(doctor): hand the config-location variables over like serve.sh

Review follow-up. serve.sh sources the checkout .env over the shell and
then replaces an unset or empty DEER_FLOW_PROJECT_ROOT with the
checkout. Doctor loaded .env shell-first and only defaulted an unset
root, so a shell DEER_FLOW_CONFIG_PATH / DEER_FLOW_PROJECT_ROOT that
differed from .env, or an empty root, made it check a different file
than the Gateway loads.

For those two variables the .env value now overrides the shell (other
keys keep doctor's shell-first loading), and an empty root becomes the
checkout, before the harness resolver runs.

* fix(doctor): report any harness import failure and expand ~ like source

Review follow-up.

- The harness-import guard caught only ImportError, so a harness that is
  present but broken differently (TypeError/SyntaxError while the module
  body runs) still crashed doctor. Catch Exception, as
  check_config_loadable does, and name the exception type in the report.
- serve.sh's `source .env` expands an unquoted leading `~` in
  DEER_FLOW_CONFIG_PATH / DEER_FLOW_PROJECT_ROOT and keeps a quoted one
  literal; dotenv_values expands neither. Expand the unquoted form so a
  `~/...` path in .env no longer fails a setup that starts, and keep the
  quoted form literal so doctor does not pass one the Gateway rejects.

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
Hyeonsang Cho 2026-09-29 17:55:21 +09:00 committed by GitHub
parent f8b69bd84b
commit 1cd42cf42e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 377 additions and 16 deletions

View File

@ -327,6 +327,17 @@ This release closes that milestone with **181 merged pull requests**.
### Fixed
- **doctor:** `make doctor` now checks the config file the Gateway actually
loads. It always inspected `<checkout>/config.yaml` and ignored
`DEER_FLOW_CONFIG_PATH` and `DEER_FLOW_PROJECT_ROOT`, so a missing override
that stops the Gateway from starting still reported `✓ config.yaml found`
and `✓ config.yaml loadable`, and a valid override pointing elsewhere got
the wrong file checked. Doctor now resolves the path through the harness's
own resolver and hands it the location variables the way `make dev` does:
`.env` values override the shell (expanding an unquoted leading `~`), and
an unset or empty `DEER_FLOW_PROJECT_ROOT` becomes the checkout. An override
the Gateway would reject fails `config.yaml found` with the Gateway's
error, and the config checks skip. ([#5987])
- **database:** `DatabaseConfig` now validates `pool_size`, `pool_recycle`, and
`command_timeout` strictly. Previously, YAML booleans (`true`/`false`) were
coerced to `1`/`0` respectively, allowing `pool_size: true` (pool size 1) and
@ -6214,4 +6225,5 @@ with **180 merged pull requests** since the first 2.0 milestone tag.
[#5964]: https://github.com/bytedance/deer-flow/pull/5964
[#5981]: https://github.com/bytedance/deer-flow/pull/5981
[#5982]: https://github.com/bytedance/deer-flow/pull/5982
[#5987]: https://github.com/bytedance/deer-flow/pull/5987

View File

@ -332,6 +332,15 @@
### 修复
- **doctor:** `make doctor` 现在检查 Gateway 实际加载的配置文件。此前它
固定检查 `<checkout>/config.yaml`,忽略 `DEER_FLOW_CONFIG_PATH` 与
`DEER_FLOW_PROJECT_ROOT`:指向不存在路径、会让 Gateway 无法启动的覆盖值仍会
显示 `✓ config.yaml found` 与 `✓ config.yaml loadable`,而指向其他有效文件的
覆盖值则会让它检查错误的文件。现在 doctor 通过 harness 自身的解析器确定路径,
并像 `make dev` 一样处理这两个位置变量:`.env` 中的值覆盖 shell 中的值(未加
引号的开头 `~` 会展开),`DEER_FLOW_PROJECT_ROOT` 未设置或为空时取仓库根目录。
Gateway 会拒绝的覆盖值会让 `config.yaml found` 失败并给出 Gateway 的错误,
其余配置检查随之跳过。([#5987])
- **数据库:** `DatabaseConfig` 现在严格校验 `pool_size`、`pool_recycle` 与
`command_timeout`。此前,YAML 布尔值(`true`/`false`)会被强制转换为 `1`/`0`,
导致 `pool_size: true`(变成仅 1 个连接)和 `command_timeout: true`(变成 1 秒超时)
@ -5272,3 +5281,4 @@ DeerFlow 2.0 是围绕"超级智能体"框架的彻底重写,核心包含子
[#5964]: https://github.com/bytedance/deer-flow/pull/5964
[#5981]: https://github.com/bytedance/deer-flow/pull/5981
[#5982]: https://github.com/bytedance/deer-flow/pull/5982
[#5987]: https://github.com/bytedance/deer-flow/pull/5987

View File

@ -53,9 +53,11 @@ application repositories continue to use `database`.
Configuration priority:
1. Explicit `config_path` argument
2. `DEER_FLOW_CONFIG_PATH` environment variable
3. `config.yaml` in current directory (backend/)
4. `config.yaml` in parent directory (project root - **recommended location**)
2. `DEER_FLOW_CONFIG_PATH` environment variable (a missing file is an error, not a fallthrough)
3. `config.yaml` under `DEER_FLOW_PROJECT_ROOT`, or the current directory when it is unset
4. Legacy `backend/config.yaml`, then repository-root `config.yaml` (project root is the **recommended location**)
`scripts/doctor.py` calls `AppConfig.resolve_config_path` rather than copying this order.
Config values starting with `$` are resolved as environment variables (e.g., `$OPENAI_API_KEY`).
`ModelConfig` also declares `use_responses_api` and `output_version` so OpenAI `/v1/responses` can be enabled explicitly while still using `langchain_openai:ChatOpenAI`.

View File

@ -6,12 +6,14 @@ Run from repo root:
from __future__ import annotations
import builtins
import importlib.util
import json
import sys
from pathlib import Path
import doctor
import pytest
REPO_ROOT = Path(__file__).resolve().parents[2]
@ -112,6 +114,103 @@ class TestCheckConfigExists:
assert result.status == "ok"
# ---------------------------------------------------------------------------
# resolve_config_path
# ---------------------------------------------------------------------------
@pytest.fixture
def runtime_path_env(monkeypatch):
"""Clear the runtime path variables and restore them after the test.
``main()`` defaults ``DEER_FLOW_PROJECT_ROOT`` in ``os.environ`` the way
``make dev`` does. ``monkeypatch.delenv`` records nothing for an unset
variable, so set each one first to make teardown remove what main() adds.
"""
for name in ("DEER_FLOW_CONFIG_PATH", "DEER_FLOW_PROJECT_ROOT"):
monkeypatch.setenv(name, "")
monkeypatch.delenv(name)
return monkeypatch
class TestResolveConfigPath:
def test_uses_config_path_env(self, tmp_path, runtime_path_env):
cfg = tmp_path / "elsewhere.yaml"
cfg.write_text("config_version: 5\n")
runtime_path_env.setenv("DEER_FLOW_CONFIG_PATH", str(cfg))
runtime_path_env.setenv("DEER_FLOW_PROJECT_ROOT", str(tmp_path / "root"))
assert doctor.resolve_config_path() == (cfg, None)
def test_missing_config_path_env_fails(self, tmp_path, runtime_path_env):
(tmp_path / "config.yaml").write_text("config_version: 5\n")
missing = tmp_path / "missing.yaml"
runtime_path_env.setenv("DEER_FLOW_CONFIG_PATH", str(missing))
runtime_path_env.setenv("DEER_FLOW_PROJECT_ROOT", str(tmp_path))
path, failure = doctor.resolve_config_path()
assert path == missing
assert failure is not None
assert failure.status == "fail"
assert "DEER_FLOW_CONFIG_PATH" in failure.detail
assert "DEER_FLOW_CONFIG_PATH" in failure.fix
def test_uses_config_under_project_root_env(self, tmp_path, runtime_path_env):
cfg = tmp_path / "config.yaml"
cfg.write_text("config_version: 5\n")
runtime_path_env.setenv("DEER_FLOW_PROJECT_ROOT", str(tmp_path))
assert doctor.resolve_config_path() == (cfg, None)
def test_missing_project_root_env_fails(self, tmp_path, runtime_path_env):
missing_root = tmp_path / "missing-root"
runtime_path_env.setenv("DEER_FLOW_PROJECT_ROOT", str(missing_root))
path, failure = doctor.resolve_config_path()
assert not path.exists()
assert failure is not None
assert failure.status == "fail"
assert "DEER_FLOW_PROJECT_ROOT" in failure.detail
assert "DEER_FLOW_PROJECT_ROOT" in failure.fix
def test_no_config_anywhere_is_a_plain_missing_config(self, tmp_path, runtime_path_env):
from deerflow.config import app_config
runtime_path_env.setattr(app_config, "_legacy_config_candidates", lambda: ())
runtime_path_env.setenv("DEER_FLOW_PROJECT_ROOT", str(tmp_path))
path, failure = doctor.resolve_config_path()
assert path == tmp_path / "config.yaml"
assert failure is None
assert doctor.check_config_exists(path).fix == "Run 'make setup' to create it"
@pytest.mark.parametrize("error", [ImportError("no module"), TypeError("ABI mismatch"), SyntaxError("bad syntax")], ids=lambda e: type(e).__name__)
def test_unimportable_harness_is_reported_not_raised(self, tmp_path, runtime_path_env, error):
# A broken backend environment is exactly what doctor must diagnose,
# whatever the harness raises while its module body executes.
real_import = builtins.__import__
def broken_import(name, *args, **kwargs):
if name == "deerflow.config.app_config":
raise error
return real_import(name, *args, **kwargs)
runtime_path_env.setattr(builtins, "__import__", broken_import)
runtime_path_env.setenv("DEER_FLOW_PROJECT_ROOT", str(tmp_path))
path, failure = doctor.resolve_config_path()
assert path == tmp_path / "config.yaml"
assert failure is not None
assert failure.status == "fail"
assert "harness" in failure.detail
assert type(error).__name__ in failure.detail
assert failure.fix == "Run 'make install'"
# ---------------------------------------------------------------------------
# check_config_version
# ---------------------------------------------------------------------------
@ -869,17 +968,35 @@ class TestCheckSandbox:
# ---------------------------------------------------------------------------
class TestMainExitCode:
def test_returns_int(self, tmp_path, monkeypatch, capsys):
"""main() should return 0 or 1 without raising."""
repo_root = tmp_path / "repo"
scripts_dir = repo_root / "scripts"
scripts_dir.mkdir(parents=True)
fake_doctor = scripts_dir / "doctor.py"
fake_doctor.write_text("# test-only shim for __file__ resolution\n")
def _fake_checkout(tmp_path: Path, monkeypatch) -> Path:
"""Point doctor at an empty checkout under ``tmp_path``.
monkeypatch.chdir(repo_root)
monkeypatch.setattr(doctor, "__file__", str(fake_doctor))
The harness's legacy config fallback is pinned to the same checkout, so
whether the real repository has a ``config.yaml`` cannot leak in.
"""
from deerflow.config import app_config
repo_root = tmp_path / "repo"
scripts_dir = repo_root / "scripts"
scripts_dir.mkdir(parents=True)
fake_doctor = scripts_dir / "doctor.py"
fake_doctor.write_text("# test-only shim for __file__ resolution\n")
monkeypatch.chdir(repo_root)
monkeypatch.setattr(doctor, "__file__", str(fake_doctor))
monkeypatch.setattr(
app_config,
"_legacy_config_candidates",
lambda: (repo_root / "backend" / "config.yaml", repo_root / "config.yaml"),
)
return repo_root
class TestMainExitCode:
def test_returns_int(self, tmp_path, runtime_path_env, capsys):
"""main() should return 0 or 1 without raising."""
monkeypatch = runtime_path_env
_fake_checkout(tmp_path, monkeypatch)
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
monkeypatch.delenv("TAVILY_API_KEY", raising=False)
@ -892,3 +1009,138 @@ class TestMainExitCode:
assert output
assert "config.yaml" in output
assert ".env" in output
class TestMainConfigResolution:
def test_missing_config_path_env_fails_even_with_a_checkout_config(self, tmp_path, runtime_path_env, capsys):
repo_root = _fake_checkout(tmp_path, runtime_path_env)
(repo_root / "config.yaml").write_text("config_version: 5\nmodels: []\n")
runtime_path_env.setenv("DEER_FLOW_CONFIG_PATH", str(tmp_path / "missing.yaml"))
exit_code = doctor.main()
output = capsys.readouterr().out
assert exit_code == 1
assert "✗ config.yaml found" in output
assert "DEER_FLOW_CONFIG_PATH" in output
# The checkout's config.yaml is not the one the Gateway would read, so
# nothing is reported about it.
assert "— config.yaml loadable" in output
assert "— models configured" in output
def test_checks_the_config_named_by_config_path_env(self, tmp_path, runtime_path_env, capsys):
_fake_checkout(tmp_path, runtime_path_env)
cfg = tmp_path / "custom.yaml"
cfg.write_text("config_version: 5\nmodels: []\n")
runtime_path_env.setenv("DEER_FLOW_CONFIG_PATH", str(cfg))
doctor.main()
output = capsys.readouterr().out
assert "✓ config.yaml found" in output
assert "✗ models configured (no models found)" in output
@pytest.mark.parametrize("project_root_env", [None, ""], ids=["unset", "empty"])
def test_defaults_project_root_to_the_checkout_like_make_dev(self, tmp_path, runtime_path_env, capsys, project_root_env):
repo_root = _fake_checkout(tmp_path, runtime_path_env)
# `make dev` runs from backend/, but serve.sh pins an unset or empty
# runtime root to the checkout, so the Gateway prefers
# <checkout>/config.yaml over the legacy backend/config.yaml.
# Resolving from the cwd would pick the backend copy instead.
if project_root_env is not None:
runtime_path_env.setenv("DEER_FLOW_PROJECT_ROOT", project_root_env)
backend_dir = repo_root / "backend"
backend_dir.mkdir()
runtime_path_env.chdir(backend_dir)
(backend_dir / "config.yaml").write_text("config_version: 5\nmodels: []\n")
(repo_root / "config.yaml").write_text("config_version: 5\nmodels:\n - name: checkout-model\n")
doctor.main()
output = capsys.readouterr().out
assert "✓ config.yaml found" in output
assert "✓ models configured (1 model(s))" in output
def test_missing_project_root_env_fails(self, tmp_path, runtime_path_env, capsys):
repo_root = _fake_checkout(tmp_path, runtime_path_env)
(repo_root / "config.yaml").write_text("config_version: 5\nmodels: []\n")
runtime_path_env.setenv("DEER_FLOW_PROJECT_ROOT", str(tmp_path / "missing-root"))
exit_code = doctor.main()
output = capsys.readouterr().out
assert exit_code == 1
assert "✗ config.yaml found" in output
assert "DEER_FLOW_PROJECT_ROOT" in output
def test_dotenv_config_path_overrides_the_shell_like_make_dev(self, tmp_path, runtime_path_env, capsys):
# serve.sh sources .env over the shell, so the Gateway loads the .env
# config even when the shell exports a different (missing) one.
repo_root = _fake_checkout(tmp_path, runtime_path_env)
cfg = tmp_path / "from-dotenv.yaml"
cfg.write_text("config_version: 5\nmodels:\n - name: dotenv-model\n")
(repo_root / ".env").write_text(f"DEER_FLOW_CONFIG_PATH={cfg}\n")
runtime_path_env.setenv("DEER_FLOW_CONFIG_PATH", str(tmp_path / "missing.yaml"))
doctor.main()
output = capsys.readouterr().out
assert "✓ config.yaml found" in output
assert "✓ models configured (1 model(s))" in output
def test_dotenv_project_root_overrides_the_shell_like_make_dev(self, tmp_path, runtime_path_env, capsys):
repo_root = _fake_checkout(tmp_path, runtime_path_env)
(repo_root / "config.yaml").write_text("config_version: 5\nmodels:\n - name: checkout-model\n")
(repo_root / ".env").write_text(f"DEER_FLOW_PROJECT_ROOT={repo_root}\n")
runtime_path_env.setenv("DEER_FLOW_PROJECT_ROOT", str(tmp_path / "missing-root"))
doctor.main()
output = capsys.readouterr().out
assert "✓ config.yaml found" in output
assert "✓ models configured (1 model(s))" in output
def test_empty_dotenv_config_path_clears_the_shell_value_like_make_dev(self, tmp_path, runtime_path_env, capsys):
# Sourcing `DEER_FLOW_CONFIG_PATH=` exports an empty value, which the
# resolver skips, so the Gateway falls back to <checkout>/config.yaml.
repo_root = _fake_checkout(tmp_path, runtime_path_env)
(repo_root / "config.yaml").write_text("config_version: 5\nmodels:\n - name: checkout-model\n")
(repo_root / ".env").write_text("DEER_FLOW_CONFIG_PATH=\n")
runtime_path_env.setenv("DEER_FLOW_CONFIG_PATH", str(tmp_path / "missing.yaml"))
doctor.main()
output = capsys.readouterr().out
assert "✓ config.yaml found" in output
assert "✓ models configured (1 model(s))" in output
@pytest.mark.parametrize(
("name", "value", "found"),
[
("DEER_FLOW_CONFIG_PATH", "~/cfg.yaml", True),
("DEER_FLOW_PROJECT_ROOT", "~/repo", True),
# bash leaves a quoted tilde literal, so the Gateway fails too.
("DEER_FLOW_CONFIG_PATH", '"~/cfg.yaml"', False),
],
ids=["config-path", "project-root", "quoted-config-path"],
)
def test_dotenv_location_tilde_expands_like_source(self, tmp_path, runtime_path_env, capsys, name, value, found):
# serve.sh's `source .env` expands an unquoted leading `~` (bash tilde
# expansion in an assignment) and keeps a quoted one literal.
repo_root = _fake_checkout(tmp_path, runtime_path_env)
for home_var in ("HOME", "USERPROFILE"):
runtime_path_env.setenv(home_var, str(tmp_path))
(tmp_path / "cfg.yaml").write_text("config_version: 5\nmodels:\n - name: home-model\n")
(repo_root / "config.yaml").write_text("config_version: 5\nmodels:\n - name: checkout-model\n")
(repo_root / ".env").write_text(f"{name}={value}\n")
exit_code = doctor.main()
output = capsys.readouterr().out
if found:
assert "✓ config.yaml found" in output
assert "✓ models configured (1 model(s))" in output
else:
assert exit_code == 1
assert "✗ config.yaml found" in output
assert "~/cfg.yaml" in output

View File

@ -40,6 +40,20 @@ where `make up` replaced the operator's secret with a generated one.
its real-Compose cases run against the installed `docker` CLI and against any
standalone binaries listed in `DEER_FLOW_TEST_COMPOSE_BINARIES`.
`doctor.py` checks the config file the Gateway would load, not a fixed
`<checkout>/config.yaml`. It mirrors how `serve.sh` hands the two
config-location variables to the Gateway: `.env` values for
`DEER_FLOW_CONFIG_PATH` / `DEER_FLOW_PROJECT_ROOT` override the shell (other
keys stay shell-first), an unquoted leading `~` in them expands as `source`
does (a quoted one stays literal), and an unset or empty
`DEER_FLOW_PROJECT_ROOT` becomes the checkout. It then asks the harness
(`AppConfig.resolve_config_path`) instead of re-implementing its order. An
override the Gateway would reject (`DEER_FLOW_CONFIG_PATH` missing,
`DEER_FLOW_PROJECT_ROOT` not a directory) fails `config.yaml found` with the
Gateway's error, and the config-dependent checks skip. Any failure to import
the harness is reported, never raised: doctor diagnoses broken environments.
Pinned by `backend/tests/test_doctor.py::TestMainConfigResolution`.
Root `make install` runs pre-commit through uv, so uv's tool bin directory
need not be on `PATH`.

View File

@ -273,6 +273,64 @@ def check_nginx() -> CheckResult:
)
# Environment variables that choose which config file the Gateway loads.
CONFIG_LOCATION_ENV_VARS = ("DEER_FLOW_CONFIG_PATH", "DEER_FLOW_PROJECT_ROOT")
def _unquoted_dotenv_keys(env_path: Path) -> set[str]:
"""Return the ``.env`` keys whose value is written without quotes."""
from dotenv.parser import parse_stream
with open(env_path, encoding="utf-8") as stream:
return {binding.key for binding in parse_stream(stream) if binding.key and binding.original.string.split("=", 1)[-1].lstrip()[:1] not in ("'", '"')}
def resolve_config_path() -> tuple[Path, CheckResult | None]:
"""Locate the config.yaml the Gateway would read.
Delegates to the harness resolver (``DEER_FLOW_CONFIG_PATH``, then
``config.yaml`` under ``DEER_FLOW_PROJECT_ROOT``, then the legacy
backend/repository-root locations) so doctor checks the same file the
Gateway loads. When an environment override would stop the Gateway from
starting, returns the path it names (which does not exist) and a failed
``config.yaml found`` result carrying the Gateway's error.
"""
config_env = os.environ.get("DEER_FLOW_CONFIG_PATH")
default_path = Path(os.environ.get("DEER_FLOW_PROJECT_ROOT") or ".") / "config.yaml"
try:
from deerflow.config.app_config import AppConfig
except Exception as exc:
# Keep diagnosing a broken backend environment instead of crashing
# (any import-time failure, as in check_config_loadable); the
# YAML-only checks still run against the most likely path.
return Path(config_env) if config_env else default_path, CheckResult(
"config.yaml found",
"fail",
f"cannot import the DeerFlow harness to resolve it ({type(exc).__name__}: {exc})",
fix="Run 'make install'",
)
try:
return AppConfig.resolve_config_path(), None
except FileNotFoundError as exc:
if not config_env:
# Nothing at the default locations: a plain missing config.
return default_path, None
return Path(config_env), CheckResult(
"config.yaml found",
"fail",
str(exc),
fix="Point DEER_FLOW_CONFIG_PATH at an existing config.yaml, or unset it",
)
except ValueError as exc:
return Path(os.environ["DEER_FLOW_PROJECT_ROOT"]) / "config.yaml", CheckResult(
"config.yaml found",
"fail",
str(exc),
fix="Point DEER_FLOW_PROJECT_ROOT at the DeerFlow checkout, or unset it",
)
def check_config_exists(config_path: Path) -> CheckResult:
if config_path.exists():
return CheckResult("config.yaml found", "ok")
@ -760,18 +818,31 @@ def check_env_file(project_root: Path) -> CheckResult:
def main() -> int:
project_root = Path(__file__).resolve().parents[1]
config_path = project_root / "config.yaml"
# Load .env early so key checks work
try:
from dotenv import load_dotenv
from dotenv import dotenv_values, load_dotenv
env_path = project_root / ".env"
if env_path.exists():
load_dotenv(env_path, override=False)
# `make dev` (scripts/serve.sh) sources .env over the shell, so
# for the variables that choose the config file, .env wins.
unquoted = _unquoted_dotenv_keys(env_path)
for name, value in dotenv_values(env_path).items():
if name in CONFIG_LOCATION_ENV_VARS and value is not None:
# `source` expands an unquoted leading `~`, not a quoted one.
os.environ[name] = os.path.expanduser(value) if name in unquoted else value
except ImportError:
pass
# serve.sh then replaces an unset or empty runtime root with the
# checkout, so config resolution and the loadable check see what the
# Gateway sees.
if not os.environ.get("DEER_FLOW_PROJECT_ROOT"):
os.environ["DEER_FLOW_PROJECT_ROOT"] = str(project_root)
config_path, config_failure = resolve_config_path()
print()
print(bold("DeerFlow Health Check"))
print("═" * 40)
@ -792,7 +863,7 @@ def main() -> int:
cfg_checks: list[CheckResult] = [
check_env_file(project_root),
check_frontend_env(project_root),
check_config_exists(config_path),
config_failure or check_config_exists(config_path),
check_config_version(config_path, project_root),
check_config_loadable(config_path),
check_models_configured(config_path),