Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions openbot
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#!/usr/bin/env bash
# Start OpenBot using the repository's production-style launcher.
set -Eeuo pipefail

usage() {
cat >&2 <<'EOF'
Usage: openbot [--root DIRECTORY] [--db-root DIRECTORY]

Start OpenBot with DIRECTORY as the workspace root and DIRECTORY/.openbot/openbot.db
as the SQLite database. DIRECTORY defaults to the current working directory; the database
root defaults to ~/.openbot.
EOF
}

error() {
printf 'openbot: %s\n' "$1" >&2
usage
exit 2
}

expand_path() {
local value="$1"
if [[ "$value" == "~" ]]; then
value="$HOME"
elif [[ "$value" == "~/"* ]]; then
value="$HOME/${value:2}"
fi
printf '%s' "$value"
}

canonical_directory() {
local value="$1"
if [[ ! -d "$value" ]]; then
error "directory does not exist or is not a directory: $value"
fi
if ! (cd -- "$value" && pwd -P); then
error "cannot access directory: $value"
fi
}

workspace_input="$(pwd -P)"
db_input="${HOME:?HOME is not set}/.openbot"
while (($#)); do
case "$1" in
--root|--db-root)
option="$1"
shift
(($#)) || error "$option requires a directory"
[[ "$1" != -* ]] || error "$option requires a directory, got: $1"
if [[ "$option" == "--root" ]]; then
workspace_input="$1"
else
db_input="$1"
fi
shift
;;
-h|--help)
usage >&2
exit 0
;;
*)
error "unknown argument: $1"
;;
esac
done

workspace_root="$(canonical_directory "$(expand_path "$workspace_input")")"
db_root="$(expand_path "$db_input")"
if [[ ! -e "$db_root" ]]; then
if ! mkdir -p -- "$db_root"; then
error "cannot create database directory: $db_root"
fi
fi
db_root="$(canonical_directory "$db_root")"

# `make run` remains the canonical production-style startup path; these environment variables
# provide the caller-specific paths without reimplementing backend initialization here.
export WORKSPACE_ROOT="$workspace_root"
export DATABASE_URL="sqlite+aiosqlite:///$db_root/openbot.db"

script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
exec make -C "$script_dir" run
78 changes: 78 additions & 0 deletions scripts/test_openbot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#!/usr/bin/env python3

import os
import subprocess
import tempfile
import unittest
from pathlib import Path

SCRIPT = Path(__file__).parents[1] / "openbot"


class OpenBotLauncherTests(unittest.TestCase):
def run_launcher(self, *args, cwd=None, home=None):
with tempfile.TemporaryDirectory() as tmp:
fake_make = Path(tmp) / "make"
fake_make.write_text(
"#!/bin/sh\n"
"printf 'cwd=%s\\n' \"$PWD\"\n"
"printf 'workspace=%s\\n' \"$WORKSPACE_ROOT\"\n"
"printf 'database=%s\\n' \"$DATABASE_URL\"\n"
"printf 'args='; printf '%s|' \"$@\"; printf '\\n'\n"
)
fake_make.chmod(0o755)
env = os.environ.copy()
env["PATH"] = f"{tmp}{os.pathsep}{env['PATH']}"
if home is not None:
env["HOME"] = str(home)
return subprocess.run(
[str(SCRIPT), *args], cwd=cwd, env=env, text=True, capture_output=True, check=False
)

def test_defaults_use_current_directory_and_home_database(self):
with tempfile.TemporaryDirectory() as tmp, tempfile.TemporaryDirectory() as home:
result = self.run_launcher(cwd=tmp, home=home)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn(f"workspace={Path(tmp).resolve()}", result.stdout)
self.assertIn(f"database=sqlite+aiosqlite:///{Path(home).resolve() / '.openbot' / 'openbot.db'}", result.stdout)
self.assertIn("args=-C|", result.stdout)

def test_root_override_preserves_spaces(self):
with tempfile.TemporaryDirectory() as tmp, tempfile.TemporaryDirectory() as home:
root = Path(tmp) / "workspace with spaces"
root.mkdir()
result = self.run_launcher("--root", str(root), cwd=tmp, home=home)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn(f"workspace={root.resolve()}", result.stdout)

def test_db_root_override_expands_tilde(self):
with tempfile.TemporaryDirectory() as tmp, tempfile.TemporaryDirectory() as home:
result = self.run_launcher("--db-root", "~/data with spaces", cwd=tmp, home=home)
self.assertEqual(result.returncode, 0, result.stderr)
expected = Path(os.path.realpath(home)) / "data with spaces" / "openbot.db"
self.assertIn(f"database=sqlite+aiosqlite:///{expected}", result.stdout)

def test_both_overrides_are_forwarded(self):
with tempfile.TemporaryDirectory() as tmp, tempfile.TemporaryDirectory() as home:
root = Path(tmp) / "root"
db = Path(tmp) / "db root"
root.mkdir()
result = self.run_launcher("--root", str(root), "--db-root", str(db), cwd=tmp, home=home)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn(f"workspace={root.resolve()}", result.stdout)
self.assertIn(f"database=sqlite+aiosqlite:///{db.resolve() / 'openbot.db'}", result.stdout)

def test_invalid_usage_is_clear(self):
result = self.run_launcher("--root")
self.assertEqual(result.returncode, 2)
self.assertIn("--root requires a directory", result.stderr)

def test_missing_directory_is_rejected(self):
with tempfile.TemporaryDirectory() as tmp:
result = self.run_launcher("--root", str(Path(tmp) / "missing"), cwd=tmp)
self.assertEqual(result.returncode, 2)
self.assertIn("does not exist", result.stderr)


if __name__ == "__main__":
unittest.main()
Loading