release: bump version to 0.74.3
Some checks failed
ci / backend (push) Has been cancelled
ci / frontend (push) Has been cancelled
ci / delivery (push) Has been cancelled
release / images (push) Has been cancelled
ci / backend (pull_request) Has been cancelled
ci / frontend (pull_request) Has been cancelled
ci / delivery (pull_request) Has been cancelled

This commit is contained in:
rayd1o
2026-09-13 02:17:55 +08:00
parent 1dd2921674
commit a54fcdbeed
17 changed files with 1160 additions and 80 deletions

View File

@@ -76,6 +76,8 @@ main() {
check_file docs/harness-audit.md
check_file docs/documentation-coverage-rules.md
check_file planet.sh
check_file scripts/lib/docker-bootstrap.zsh
check_file scripts/harness/test_docker_bootstrap.py
check_file pyproject.toml
check_file frontend/package.json
check_file scripts/harness/security-check.sh

View File

@@ -13,6 +13,9 @@ main() {
harness_run scripts/harness/doctor.sh
harness_run git diff --check
harness_run zsh -n planet.sh
harness_run zsh -n scripts/lib/docker-bootstrap.zsh
harness_run "$uv_bin" run --frozen --project "$ROOT_DIR" python scripts/harness/test_docker_bootstrap.py
harness_run "$uv_bin" run --frozen --project "$ROOT_DIR" python scripts/harness/test_database_startup.py
harness_run bash -n scripts/bootstrap-dev.sh
harness_run bash -n scripts/harness/lib.sh
harness_run bash -n scripts/harness/doctor.sh

View File

@@ -0,0 +1,274 @@
"""Database lifecycle regressions without starting or changing host containers."""
import asyncio
from contextlib import redirect_stderr, redirect_stdout
import io
import json
from pathlib import Path
import re
import subprocess
import sys
import unittest
from unittest.mock import AsyncMock, MagicMock, patch
from asyncpg import InvalidCatalogNameError, InvalidPasswordError
from sqlalchemy.engine import make_url
from sqlalchemy.exc import DBAPIError
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "scripts"))
import check_database_connection as probe # noqa: E402
TEST_URL = make_url("postgresql+asyncpg://postgres:test-secret@localhost:5432/planet_db")
def shell_function(name: str) -> str:
source = (ROOT / "planet.sh").read_text()
match = re.search(rf"^{name}\(\) \{{\n.*?^\}}", source, re.MULTILINE | re.DOTALL)
if match is None:
raise AssertionError(f"missing shell function: {name}")
return match.group()
def run_shell(functions: list[str], setup: str, action: str) -> subprocess.CompletedProcess[str]:
return subprocess.run(
["zsh", "-f"],
input="\n".join(["set -e", *(shell_function(name) for name in functions), setup, action]),
capture_output=True,
text=True,
timeout=10,
check=False,
)
class DatabaseLifecycleTests(unittest.TestCase):
def test_existing_container_gets_current_compose_configuration(self) -> None:
for function in ("start_database_services", "start_postgres_service"):
with self.subTest(function=function):
result = run_shell(
[function],
"""
mapped=0
docker() { return 0; }
compose_up() { mapped=1; }
""",
f"{function}\n[ $mapped -eq 1 ]",
)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
def test_port_conflict_is_visible_and_cannot_fall_back_to_old_container(self) -> None:
result = run_shell(
["start_database_services"],
"""
docker() { return 0; }
compose_up() { echo 'port is already allocated' >&2; return 1; }
""",
"start_database_services",
)
self.assertNotEqual(result.returncode, 0)
self.assertIn("port is already allocated", result.stderr)
def test_installed_compose_failure_is_not_reported_as_missing_compose(self) -> None:
result = run_shell(
["compose_up"],
"""
compose_available() { return 0; }
compose_v1_available() { return 1; }
docker() { echo 'address already in use' >&2; return 1; }
set_wait_detail() { :; }
log_warn() { echo "$*"; }
log_error() { echo "$*"; }
report_missing_compose() { echo MISSING_COMPOSE; return 1; }
""",
"compose_up up -d postgres",
)
self.assertNotEqual(result.returncode, 0)
self.assertIn("address already in use", result.stderr)
self.assertNotIn("MISSING_COMPOSE", result.stdout)
def run_init(self, connection_status: int) -> subprocess.CompletedProcess[str]:
return run_shell(
["init"],
f"""
for fn in parse_service_args guard_init_when_services_running print_splash \
start_wait_session stop_wait_session ensure_docker_runtime ensure_python_runtime \
sync_python_deps sync_frontend_deps ensure_planet_env_files \
prepare_motion_agent_host_dependencies set_wait_detail; do
functions[$fn]='return 0'
done
log_success() {{ echo "$*"; }}
log_error() {{ echo "$*"; }}
log_step() {{ :; }}
log_note() {{ :; }}
ensure_database_services_healthy() {{ echo CONTAINERS_HEALTHY; }}
verify_backend_database_connection() {{ echo CONNECTION_CHECK; return {connection_status}; }}
run_command_with_spinner() {{ shift; "$@"; }}
initialize_backend_database() {{ echo SCHEMA_INITIALIZED; }}
""",
"init --non-motion-agent",
)
def test_healthy_containers_do_not_allow_schema_changes_on_bad_connection(self) -> None:
result = self.run_init(1)
self.assertNotEqual(result.returncode, 0, result.stdout)
self.assertIn("CONNECTION_CHECK", result.stdout)
self.assertNotIn("SCHEMA_INITIALIZED", result.stdout)
self.assertNotIn("数据库服务已就绪", result.stdout)
def test_real_connection_is_checked_before_reporting_ready_and_creating_tables(self) -> None:
result = self.run_init(0)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn("CONNECTION_CHECK", result.stdout)
self.assertLess(
result.stdout.index("CONNECTION_CHECK"), result.stdout.index("数据库服务已就绪")
)
self.assertLess(
result.stdout.index("数据库服务已就绪"), result.stdout.index("SCHEMA_INITIALIZED")
)
def test_only_missing_ports_trigger_one_volume_preserving_recreation(self) -> None:
for first_status, retry_status, expected_status, expected_recreates in (
(0, 0, 0, 0),
(1, 0, 1, 0),
(2, 0, 0, 1),
(2, 2, 2, 1),
):
with self.subTest(first_status=first_status, retry_status=retry_status):
result = run_shell(
["verify_backend_database_connection"],
f"""
checks=0
set_wait_detail() {{ :; }}
log_warn() {{ :; }}
compose_up() {{ echo "COMPOSE $*"; }}
wait_for_postgres_health() {{ return 0; }}
run_command_with_spinner() {{
checks=$((checks + 1))
if [ $checks -eq 1 ]; then return {first_status}; fi
return {retry_status}
}}
""",
"verify_backend_database_connection",
)
self.assertEqual(result.returncode, expected_status, result.stderr)
self.assertEqual(result.stdout.count("COMPOSE"), expected_recreates)
if expected_recreates:
self.assertIn("up -d --no-deps --force-recreate postgres", result.stdout)
self.assertNotIn("--renew-anon-volumes", result.stdout)
class DatabaseProbeTests(unittest.TestCase):
def docker_result(
self, ports: dict | None, mode: str = "bridge"
) -> subprocess.CompletedProcess:
return subprocess.CompletedProcess([], 0, f"{mode}\n{json.dumps(ports)}\n", "")
def test_missing_port_blocks_sql_even_if_container_is_healthy(self) -> None:
output = io.StringIO()
with (
patch.object(probe, "backend_database_url", return_value=TEST_URL),
patch.object(probe.subprocess, "run", return_value=self.docker_result({})),
patch.object(probe, "check_connection", new_callable=AsyncMock) as connect,
redirect_stderr(output),
redirect_stdout(output),
):
self.assertEqual(probe.main(), 2)
connect.assert_not_called()
self.assertIn("未向宿主机发布", output.getvalue())
self.assertNotIn("test-secret", output.getvalue())
def test_actual_backend_port_must_match_published_port(self) -> None:
result = self.docker_result({"5432/tcp": [{"HostIp": "0.0.0.0", "HostPort": "15432"}]})
with patch.object(probe.subprocess, "run", return_value=result):
with self.assertRaises(probe.DatabaseReadinessError):
probe.check_published_port(TEST_URL)
probe.check_published_port(TEST_URL.set(port=15432))
def test_host_networking_does_not_require_a_port_mapping(self) -> None:
with patch.object(probe.subprocess, "run", return_value=self.docker_result(None, "host")):
probe.check_published_port(TEST_URL)
def test_external_database_does_not_require_a_local_mapping(self) -> None:
with patch.object(probe.subprocess, "run") as inspect:
probe.check_published_port(TEST_URL.set(host="configured-db.example"))
inspect.assert_not_called()
def test_docker_failure_does_not_echo_raw_output(self) -> None:
result = subprocess.CompletedProcess([], 1, "", "sensitive test-secret")
with patch.object(probe.subprocess, "run", return_value=result):
with self.assertRaises(probe.DatabaseReadinessError) as error:
probe.check_published_port(TEST_URL)
self.assertIn("Docker context", str(error.exception))
self.assertNotIn("test-secret", str(error.exception))
def test_authentication_error_is_actionable_without_leaking_dsn_or_query_secrets(self) -> None:
output = io.StringIO()
url = TEST_URL.update_query_dict({"sslpassword": "query-secret"})
error = DBAPIError(
None, None, InvalidPasswordError(url.render_as_string(hide_password=False))
)
with (
patch.object(probe, "backend_database_url", return_value=url),
patch.object(probe, "check_published_port"),
patch.object(probe, "check_connection", new_callable=AsyncMock, side_effect=error),
redirect_stderr(output),
redirect_stdout(output),
):
self.assertEqual(probe.main(), 1)
self.assertIn("认证失败", output.getvalue())
self.assertNotIn("test-secret", output.getvalue())
self.assertNotIn("query-secret", output.getvalue())
self.assertNotIn("Traceback", output.getvalue())
def test_missing_database_and_network_errors_have_distinct_diagnostics(self) -> None:
self.assertIn("数据库不存在", probe.connection_diagnostic(InvalidCatalogNameError()))
for error in (ConnectionRefusedError(), TimeoutError()):
self.assertIn("地址不可达", probe.connection_diagnostic(error))
def test_invalid_config_never_echoes_validation_input(self) -> None:
output = io.StringIO()
with (
patch.object(probe, "backend_database_url", side_effect=ValueError("test-secret")),
redirect_stderr(output),
):
self.assertEqual(probe.main(), 1)
self.assertNotIn("test-secret", output.getvalue())
class DatabaseConnectionTests(unittest.IsolatedAsyncioTestCase):
async def test_connection_deadline_stops_a_stalled_probe(self) -> None:
async def stall() -> None:
await asyncio.sleep(1)
engine = MagicMock()
engine.dispose = AsyncMock()
engine.connect.return_value.__aenter__.side_effect = stall
with (
patch.object(probe, "create_async_engine", return_value=engine),
patch.object(probe, "CONNECT_TIMEOUT_SECONDS", 0.001),
):
with self.assertRaises(TimeoutError):
await probe.check_connection(TEST_URL)
engine.dispose.assert_awaited_once()
async def test_probe_only_selects_and_always_disposes_the_engine(self) -> None:
for error in (None, InvalidPasswordError("test-secret")):
with self.subTest(error=type(error).__name__):
engine = MagicMock()
engine.dispose = AsyncMock()
connection = AsyncMock()
engine.connect.return_value.__aenter__.return_value = connection
connection.execute.side_effect = error
with patch.object(probe, "create_async_engine", return_value=engine):
if error:
with self.assertRaises(InvalidPasswordError):
await probe.check_connection(TEST_URL)
else:
await probe.check_connection(TEST_URL)
self.assertEqual(str(connection.execute.call_args.args[0]), "SELECT 1")
engine.dispose.assert_awaited_once()
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,332 @@
"""Exercise Docker bootstrap with isolated command stubs, never the host daemon."""
import json
import os
from pathlib import Path
import shlex
import shutil
import subprocess
import sys
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[2]
MODULE = ROOT / "scripts/lib/docker-bootstrap.zsh"
COMMAND_STUB = r"""
import json
import os
from pathlib import Path
import subprocess
import sys
state_path = Path(os.environ["DOCKER_TEST_STATE"])
state = json.loads(state_path.read_text())
name = Path(sys.argv[0]).name
args = sys.argv[1:]
state["calls"].append([name, args])
def save() -> None:
state_path.write_text(json.dumps(state))
def finish(code: int = 0, output: str = "") -> None:
save()
if output:
print(output)
sys.exit(code)
def install_command(name: str) -> None:
target = state_path.parent / "bin" / name
if not target.exists():
target.symlink_to("mock-command")
if name == "id":
if args == ["-u"]:
finish(output=str(state["uid"]))
if args == ["-un"]:
finish(output="planet-test")
finish(output="planet-test" + (" docker" if state["member"] else ""))
if name == "sudo":
if state.get("sudo_denied"):
finish(1)
if args == ["-v"]:
finish()
if "-E" in args:
finish() # Record re-exec argv without executing planet.sh on this machine.
save()
sys.exit(subprocess.call(args[1:] if args[0] == "--" else args))
if name == "apt-get":
if state.get("apt_fail"):
finish(1, "simulated apt failure")
if args[0] == "install":
for package in args[2:]:
if package in ("docker.io", "docker-ce"):
state["engine"] = True
install_command("docker")
install_command("dockerd")
if package in ("docker-compose-v2", "docker-compose-plugin"):
state["compose"] = True
if package in ("docker-buildx", "docker-buildx-plugin"):
state["buildx"] = "0.20.0"
if package == "passwd":
install_command("usermod")
finish()
if name == "dpkg-query":
finish(0 if state.get("ce") else 1, "install ok installed" if state.get("ce") else "")
if name == "systemctl":
if args[0] == "show":
unit_exists = state["engine"] and (args[-1] != "docker.socket" or state["socket_unit"])
finish(output="loaded" if unit_exists else "not-found")
if args[0] == "is-active":
finish(output="active" if state["running"] else "inactive")
if state.get("service_fail"):
finish(1, "simulated service failure")
state["running"] = True
finish()
if name == "usermod":
state["member"] = True
finish()
if name == "mock-socket-access":
finish(0 if state["running"] and state["uid"] != 0 and not state["access"] else 1)
if name == "docker":
if args[0] == "info":
finish(0 if state["running"] and (state["uid"] == 0 or state["access"]) else 1)
if args[:2] == ["context", "inspect"]:
finish(output=state["endpoint"])
if args[:2] == ["compose", "version"]:
finish(0 if state["compose"] else 1)
if args[:2] == ["buildx", "version"]:
finish(0 if state["buildx"] else 1, "github.com/docker/buildx v" + state["buildx"])
finish(1)
"""
class DockerBootstrapTests(unittest.TestCase):
def setUp(self) -> None:
self.temporary = tempfile.TemporaryDirectory(prefix="planet-docker-test-")
self.addCleanup(self.temporary.cleanup)
self.folder = Path(self.temporary.name)
self.bin = self.folder / "bin"
self.bin.mkdir()
self.state_file = self.folder / "state.json"
self.state = {
"calls": [],
"uid": 1000,
"member": False,
"access": False,
"engine": False,
"running": False,
"compose": False,
"buildx": "",
"endpoint": "unix:///var/run/docker.sock",
"socket_unit": True,
}
stub = self.bin / "mock-command"
stub.write_text(f"#!{sys.executable}\n" + COMMAND_STUB)
stub.chmod(0o755)
for name in ("id", "sudo", "apt-get", "systemctl", "dpkg-query", "mock-socket-access"):
(self.bin / name).symlink_to("mock-command")
for name in ("zsh", "env", "sort", "head", "tail", "readlink", "awk", "sed"):
executable = shutil.which(name)
self.assertIsNotNone(executable, f"test prerequisite missing: {name}")
(self.bin / name).symlink_to(executable)
def existing_engine(self, *, running: bool = True, access: bool = True) -> None:
for name in ("docker", "dockerd"):
(self.bin / name).symlink_to("mock-command")
self.state.update(
engine=True, running=running, access=access, compose=True, buildx="0.20.0"
)
def run_bootstrap(
self,
action: str = "ensure_docker_runtime",
*,
extra_env: dict[str, str] | None = None,
arguments: list[str] | None = None,
) -> subprocess.CompletedProcess[str]:
self.state_file.write_text(json.dumps(self.state))
env = os.environ.copy()
for name in ("DOCKER_HOST", "DOCKER_CONTEXT", "PLANET_DOCKER_GROUP_REEXEC"):
env.pop(name, None)
env.update(
PATH=str(self.bin),
DOCKER_TEST_STATE=str(self.state_file),
TEST_OS="ubuntu",
TEST_DESKTOP="0",
PLANET_STATE_DIR=str(self.folder),
)
env.update(extra_env or {})
arguments = arguments or ["init", "--non-motion-agent"]
prelude = f"""
set -e
SCRIPT_DIR={shlex.quote(str(self.folder / "repo with ' quotes"))}
source {shlex.quote(str(MODULE))}
log_note() {{ print -r -- "$*"; }}
log_error() {{ print -r -- "$*"; }}
log_step() {{ print -r -- "$*"; }}
docker_bootstrap_os_id() {{ print -r -- "$TEST_OS"; }}
docker_desktop_present() {{ [[ "$TEST_DESKTOP" == 1 ]]; }}
docker_socket_needs_group_access() {{ mock-socket-access; }}
run_command_quiet_unless_verbose() {{ local output="$1"; shift; "$@" > "$output" 2>&1; }}
ensure_system_command() {{ command -v "$1" >/dev/null || docker_as_root apt-get install -y "$2"; }}
{action} {shlex.join(arguments)}
"""
result = subprocess.run(
[str(self.bin / "zsh"), "-f", "-c", prelude],
env=env,
text=True,
capture_output=True,
timeout=20,
)
self.state = json.loads(self.state_file.read_text())
return result
def calls(self, name: str) -> list[list[str]]:
return [args for command, args in self.state["calls"] if command == name]
def test_working_environment_is_reused_without_privilege_or_installation(self) -> None:
self.existing_engine()
result = self.run_bootstrap()
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertEqual(self.calls("apt-get"), [])
self.assertEqual(self.calls("sudo"), [])
self.assertEqual(self.calls("systemctl"), [])
def test_fresh_root_install_starts_and_validates_engine(self) -> None:
self.state["uid"] = 0
result = self.run_bootstrap()
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertEqual(
self.calls("apt-get"),
[
["update", "--error-on=any"],
["install", "-y", "docker.io", "docker-compose-v2", "docker-buildx"],
],
)
self.assertIn(["enable", "--now", "docker.service"], self.calls("systemctl"))
self.assertTrue(self.state["running"])
def test_fresh_user_installs_missing_usermod_and_reexecutes_without_sg(self) -> None:
args = ["--verbose", "init", "--non-motion-agent", "a 'quoted' $(argument)"]
result = self.run_bootstrap(arguments=args)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertIn(["install", "-y", "passwd"], self.calls("apt-get"))
self.assertEqual(self.calls("usermod"), [["-aG", "docker", "planet-test"]])
reexec = self.calls("sudo")[-1]
self.assertEqual(reexec[:7], ["-E", "-u", "planet-test", "-g", "docker", "--", "env"])
self.assertEqual(reexec[7], "PLANET_DOCKER_GROUP_REEXEC=1")
self.assertEqual(reexec[8], f"PATH={self.bin}")
self.assertEqual(reexec[10], str(self.folder / "repo with ' quotes/planet.sh"))
self.assertEqual(reexec[11:], args)
self.assertFalse((self.bin / "sg").exists())
def test_existing_group_membership_refreshes_start_without_installing(self) -> None:
self.existing_engine(access=False)
self.state["member"] = True
result = self.run_bootstrap("refresh_docker_group", arguments=["start", "--allow-lan"])
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertEqual(self.calls("sudo")[-1][-2:], ["start", "--allow-lan"])
self.assertEqual(self.calls("apt-get"), [])
def test_failed_group_refresh_does_not_loop(self) -> None:
self.existing_engine(access=False)
self.state["member"] = True
result = self.run_bootstrap(
"refresh_docker_group", extra_env={"PLANET_DOCKER_GROUP_REEXEC": "1"}
)
self.assertNotEqual(result.returncode, 0)
self.assertEqual(self.calls("sudo"), [])
def test_stopped_existing_engine_is_started_without_installation(self) -> None:
self.existing_engine(running=False)
result = self.run_bootstrap()
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertEqual(self.calls("apt-get"), [])
self.assertTrue(self.state["running"])
def test_missing_or_outdated_plugins_are_installed_without_replacing_engine(self) -> None:
self.existing_engine()
self.state.update(compose=False, buildx="0.16.0")
result = self.run_bootstrap()
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertEqual(
self.calls("apt-get")[-1], ["install", "-y", "docker-compose-v2", "docker-buildx"]
)
def test_existing_docker_ce_keeps_its_package_family(self) -> None:
self.existing_engine()
self.state.update(compose=False, buildx="", ce=True)
result = self.run_bootstrap()
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertEqual(
self.calls("apt-get")[-1],
["install", "-y", "docker-compose-plugin", "docker-buildx-plugin"],
)
def test_install_failure_stops_before_daemon_start(self) -> None:
self.state["apt_fail"] = True
result = self.run_bootstrap()
self.assertNotEqual(result.returncode, 0)
self.assertEqual(self.calls("systemctl"), [])
self.assertIn("simulated apt failure", result.stdout)
def test_missing_sudo_reports_requirement_without_attempting_install(self) -> None:
(self.bin / "sudo").unlink()
result = self.run_bootstrap()
self.assertNotEqual(result.returncode, 0)
self.assertIn("缺少 sudo", result.stdout)
self.assertEqual(self.calls("apt-get"), [])
def test_desktop_and_nonlocal_contexts_are_not_replaced(self) -> None:
result = self.run_bootstrap(extra_env={"TEST_DESKTOP": "1"})
self.assertNotEqual(result.returncode, 0)
self.assertIn("Docker Desktop", result.stdout)
self.assertEqual(self.calls("apt-get"), [])
result = self.run_bootstrap(extra_env={"DOCKER_HOST": "ssh://remote-docker"})
self.assertNotEqual(result.returncode, 0)
self.assertEqual(self.calls("apt-get"), [])
def test_working_remote_environment_is_reused(self) -> None:
self.existing_engine()
result = self.run_bootstrap(extra_env={"DOCKER_HOST": "ssh://remote-docker"})
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertEqual(self.calls("sudo"), [])
def test_unsupported_os_has_an_explicit_error(self) -> None:
result = self.run_bootstrap(extra_env={"TEST_OS": "debian"})
self.assertNotEqual(result.returncode, 0)
self.assertIn("自动安装目前支持 Ubuntu", result.stdout)
self.assertEqual(self.calls("apt-get"), [])
def test_missing_cli_diagnostic_never_suggests_a_socket_unit(self) -> None:
result = self.run_bootstrap("log_docker_daemon_diagnostics")
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertIn("未找到 Docker CLI", result.stdout)
self.assertNotIn("systemctl", result.stdout)
self.assertEqual(self.calls("systemctl"), [])
def test_missing_service_diagnostic_never_suggests_a_socket_unit(self) -> None:
(self.bin / "docker").symlink_to("mock-command")
result = self.run_bootstrap("log_docker_daemon_diagnostics")
self.assertIn("未检测到可用的 docker.service", result.stdout)
self.assertNotIn("enable --now docker.socket", result.stdout)
def test_service_without_socket_unit_gets_service_only_hint(self) -> None:
self.existing_engine(running=False)
self.state["socket_unit"] = False
result = self.run_bootstrap("log_docker_daemon_diagnostics")
self.assertIn("enable --now docker.service", result.stdout)
self.assertNotIn("enable --now docker.socket", result.stdout)
def test_daemon_start_failure_is_not_reported_as_database_failure(self) -> None:
self.existing_engine(running=False)
self.state["service_fail"] = True
result = self.run_bootstrap()
self.assertNotEqual(result.returncode, 0)
self.assertIn("Docker Engine 启动失败", result.stdout)
self.assertNotIn("数据库启动失败", result.stdout)
if __name__ == "__main__":
unittest.main(verbosity=2)