| import unittest |
| from pathlib import Path |
|
|
|
|
| class RuntimeUserDefaultsTests(unittest.TestCase): |
| @staticmethod |
| def _repo_root() -> Path: |
| return Path(__file__).resolve().parents[1] |
|
|
| @classmethod |
| def _dockerfile_path(cls) -> Path: |
| return cls._repo_root() / "Dockerfile" |
|
|
| @classmethod |
| def _entrypoint_path(cls) -> Path: |
| return cls._repo_root() / "scripts" / "openclaw-entrypoint.sh" |
|
|
| def test_dockerfile_defaults_to_root_runtime_home(self): |
| dockerfile = self._dockerfile_path().read_text(encoding="utf-8") |
| self.assertIn("ARG OPENCLAW_VERSION", dockerfile) |
| self.assertIn("ENV OPENCLAW_VERSION=$OPENCLAW_VERSION", dockerfile) |
| self.assertIn('OPENCLAW_VERSION_VALUE="${OPENCLAW_VERSION:-latest}"', dockerfile) |
| self.assertIn("ENV HOME=/root", dockerfile) |
| self.assertIn("ENV OPENCLAW_HOME=/root", dockerfile) |
| self.assertIn("ENV OPENCLAW_USER=root", dockerfile) |
| self.assertIn("ENV OPENCLAW_GROUP=root", dockerfile) |
| self.assertIn("ENV OPENCLAW_BACKUP_SOURCE_DIR=/root/.openclaw", dockerfile) |
| self.assertIn("ENV OPENCLAW_BACKUP_ROOT_CONFIG_DIR=/root/.config", dockerfile) |
| self.assertIn("ENV OPENCLAW_BACKUP_ROOT_CODEX_DIR=/root/.codex", dockerfile) |
| self.assertIn("ENV OPENCLAW_BACKUP_ROOT_CLAUDE_DIR=/root/.claude", dockerfile) |
| self.assertIn("ENV OPENCLAW_BACKUP_ROOT_AGENTS_DIR=/root/.agents", dockerfile) |
| self.assertIn("ENV OPENCLAW_BACKUP_ROOT_SSH_DIR=/root/.ssh", dockerfile) |
| self.assertIn("ENV OPENCLAW_BACKUP_ROOT_ENV_DIR=/root/.env.d", dockerfile) |
| self.assertIn("ENV OPENCLAW_BACKUP_ROOT_NPM_DIR=/root/.npm", dockerfile) |
| self.assertIn("ENV OPENCLAW_BACKUP_ROOT_LARK_CLI_DIR=/root/.lark-cli", dockerfile) |
| self.assertIn('OPENCLAW_BIN="$(command -v openclaw || true)"', dockerfile) |
| self.assertIn('if [[ -z "$OPENCLAW_BIN" ]] && [[ -x /root/.npm-global/bin/openclaw ]]; then', dockerfile) |
| self.assertIn('elif [[ -z "$OPENCLAW_BIN" ]] && [[ -x /root/.local/bin/openclaw ]]; then', dockerfile) |
| self.assertIn("if [ -x /root/.local/bin/sshx ]; then", dockerfile) |
| self.assertNotIn("/home/ubuntu/.npm-global/bin/openclaw", dockerfile) |
| self.assertNotIn("/home/ubuntu/.local/bin/sshx", dockerfile) |
|
|
| def test_dockerfile_does_not_create_ubuntu_user_or_sudoer(self): |
| dockerfile = self._dockerfile_path().read_text(encoding="utf-8") |
| self.assertNotIn("useradd --system --create-home --gid ubuntu --shell /bin/bash ubuntu", dockerfile) |
| self.assertNotIn("ubuntu ALL=(ALL) NOPASSWD:ALL", dockerfile) |
| self.assertNotIn("/etc/sudoers.d/90-ubuntu-nopasswd", dockerfile) |
|
|
| def test_dockerfile_preinstalls_common_cli_tools(self): |
| dockerfile = self._dockerfile_path().read_text(encoding="utf-8") |
| self.assertIn("python3", dockerfile) |
| self.assertIn("vim \\", dockerfile) |
| self.assertIn("neovim", dockerfile) |
| self.assertNotIn("apt-get install -y --no-install-recommends \\\n ca-certificates \\\n chromium-headless-shell", dockerfile) |
| self.assertIn("googlechromelabs.github.io/chrome-for-testing/LATEST_RELEASE_STABLE", dockerfile) |
| self.assertIn("chrome-${CFT_ARCH}.zip", dockerfile) |
| self.assertIn('CFT_DIR="chrome-${CFT_ARCH}"', dockerfile) |
| self.assertIn('ln -sf "/opt/${CFT_DIR}/chrome" /usr/local/bin/chromium', dockerfile) |
| self.assertIn("gh", dockerfile) |
| self.assertIn( |
| "npm install -g --no-audit --no-fund opencode-ai @openai/codex @anthropic-ai/claude-code", |
| dockerfile, |
| ) |
| self.assertIn("for cmd in opencode codex claude; do", dockerfile) |
| self.assertIn('ln -sf "$CLI_BIN" "/usr/local/bin/$cmd"', dockerfile) |
| self.assertIn('"/usr/local/bin/$cmd" --help >/dev/null', dockerfile) |
| self.assertIn("npx skills add larksuite/cli -y -g", dockerfile) |
| self.assertIn('hf --help >/dev/null', dockerfile) |
| self.assertIn("uv --version >/dev/null", dockerfile) |
|
|
| def test_entrypoint_defaults_to_root_runtime_user(self): |
| entrypoint = self._entrypoint_path().read_text(encoding="utf-8") |
| self.assertIn('OPENCLAW_USER="${OPENCLAW_USER:-root}"', entrypoint) |
| self.assertIn('OPENCLAW_GROUP="${OPENCLAW_GROUP:-root}"', entrypoint) |
| self.assertIn('OPENCLAW_HOME="${OPENCLAW_HOME:-/root}"', entrypoint) |
| self.assertIn('OPENCLAW_BACKUP_SOURCE_DIR="${OPENCLAW_BACKUP_SOURCE_DIR:-${OPENCLAW_STATE_DIR}}"', entrypoint) |
| self.assertIn('OPENCLAW_BACKUP_ROOT_CONFIG_DIR="${OPENCLAW_BACKUP_ROOT_CONFIG_DIR:-/root/.config}"', entrypoint) |
| self.assertIn('OPENCLAW_BACKUP_ROOT_CODEX_DIR="${OPENCLAW_BACKUP_ROOT_CODEX_DIR:-/root/.codex}"', entrypoint) |
| self.assertIn('OPENCLAW_BACKUP_ROOT_CLAUDE_DIR="${OPENCLAW_BACKUP_ROOT_CLAUDE_DIR:-/root/.claude}"', entrypoint) |
| self.assertIn('OPENCLAW_BACKUP_ROOT_AGENTS_DIR="${OPENCLAW_BACKUP_ROOT_AGENTS_DIR:-/root/.agents}"', entrypoint) |
| self.assertIn('OPENCLAW_BACKUP_ROOT_SSH_DIR="${OPENCLAW_BACKUP_ROOT_SSH_DIR:-/root/.ssh}"', entrypoint) |
| self.assertIn('OPENCLAW_BACKUP_ROOT_ENV_DIR="${OPENCLAW_BACKUP_ROOT_ENV_DIR:-/root/.env.d}"', entrypoint) |
| self.assertIn('OPENCLAW_BACKUP_ROOT_NPM_DIR="${OPENCLAW_BACKUP_ROOT_NPM_DIR:-/root/.npm}"', entrypoint) |
| self.assertIn('OPENCLAW_BACKUP_ROOT_LARK_CLI_DIR="${OPENCLAW_BACKUP_ROOT_LARK_CLI_DIR:-/root/.lark-cli}"', entrypoint) |
|
|
| def test_backup_cron_uses_openclaw_user(self): |
| entrypoint = self._entrypoint_path().read_text(encoding="utf-8") |
| self.assertIn( |
| '${OPENCLAW_BACKUP_CRON} ${OPENCLAW_USER} /usr/local/bin/openclaw-backup-cron.sh', |
| entrypoint, |
| ) |
| self.assertNotIn( |
| "${OPENCLAW_BACKUP_CRON} root /usr/local/bin/openclaw-backup-cron.sh", |
| entrypoint, |
| ) |
|
|
| def test_entrypoint_does_not_rechown_entire_openclaw_home(self): |
| entrypoint = self._entrypoint_path().read_text(encoding="utf-8") |
| self.assertNotIn('find "$OPENCLAW_HOME" -xdev -exec chown "$OPENCLAW_USER:$OPENCLAW_GROUP" {} +', entrypoint) |
| self.assertNotIn('chown -R "$OPENCLAW_USER:$OPENCLAW_GROUP" "$OPENCLAW_HOME"', entrypoint) |
| self.assertNotIn(' "$OPENCLAW_HOME" \\', entrypoint) |
|
|
| def test_backup_cron_setup_skips_when_not_root(self): |
| entrypoint = self._entrypoint_path().read_text(encoding="utf-8") |
| self.assertIn( |
| 'setup_backup_cron() {\n if [[ "$(id -u)" -ne 0 ]]; then', |
| entrypoint, |
| ) |
|
|
| def test_backup_env_file_includes_keep_count(self): |
| entrypoint = self._entrypoint_path().read_text(encoding="utf-8") |
| self.assertIn("OPENCLAW_BACKUP_KEEP_COUNT", entrypoint) |
| self.assertIn("OPENCLAW_BACKUP_ROOT_CONFIG_DIR", entrypoint) |
| self.assertIn("OPENCLAW_BACKUP_ROOT_CODEX_DIR", entrypoint) |
| self.assertIn("OPENCLAW_BACKUP_ROOT_CLAUDE_DIR", entrypoint) |
| self.assertIn("OPENCLAW_BACKUP_ROOT_AGENTS_DIR", entrypoint) |
| self.assertIn("OPENCLAW_BACKUP_ROOT_SSH_DIR", entrypoint) |
| self.assertIn("OPENCLAW_BACKUP_ROOT_ENV_DIR", entrypoint) |
| self.assertIn("OPENCLAW_BACKUP_ROOT_NPM_DIR", entrypoint) |
| self.assertIn("OPENCLAW_BACKUP_ROOT_LARK_CLI_DIR", entrypoint) |
|
|
| def test_sshx_auto_start_does_not_use_command_substitution(self): |
| entrypoint = self._entrypoint_path().read_text(encoding="utf-8") |
| self.assertNotIn('OPENCLAW_SSHX_PID="$(run_as_node_background_stdout sshx)"', entrypoint) |
|
|
| def test_sshx_logs_directly_to_container_stdout_stderr(self): |
| entrypoint = self._entrypoint_path().read_text(encoding="utf-8") |
| self.assertIn('gosu "$OPENCLAW_USER:$OPENCLAW_GROUP" sshx >/proc/1/fd/1 2>/proc/1/fd/2 &', entrypoint) |
| self.assertIn("sshx >/proc/1/fd/1 2>/proc/1/fd/2 &", entrypoint) |
| self.assertNotIn('gosu "$OPENCLAW_USER:$OPENCLAW_GROUP" sshx >>"$OPENCLAW_STDOUT_LOG_PATH" 2>>"$OPENCLAW_STDERR_LOG_PATH" &', entrypoint) |
| self.assertNotIn('sshx >>"$OPENCLAW_STDOUT_LOG_PATH" 2>>"$OPENCLAW_STDERR_LOG_PATH" &', entrypoint) |
|
|
| def test_docker_healthcheck_does_not_send_gateway_token_header(self): |
| dockerfile = self._dockerfile_path().read_text(encoding="utf-8") |
| self.assertIn("HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3", dockerfile) |
| self.assertIn("curl -f http://localhost:7860", dockerfile) |
| self.assertNotIn("x-openclaw-token", dockerfile) |
|
|
|
|
| if __name__ == "__main__": |
| unittest.main() |
|
|