Add learner environment integration tests

- test-learner-env.sh: SSH, nix-ld, Slack tokens, Python, API connectivity
- test-slack-bolt.py: Socket Mode connection test
- Makefile: test runner with env/slack-bolt/vscode targets
- Add python3 + uv to system packages

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Dan 2026-01-02 12:16:58 -08:00
parent 21c356979e
commit d387b0b910
4 changed files with 349 additions and 0 deletions

View file

@ -24,6 +24,9 @@
git
htop
curl
# Python for learner bot development
python3
uv
];
# Enable Nix flakes

55
tests/Makefile Normal file
View file

@ -0,0 +1,55 @@
# Integration tests for learner dev environment
# Run from repo root: make -C tests <target>
SERVER := 45.77.205.49
USER := dantest
.PHONY: all env slack-bolt vscode help
help:
@echo "Learner Environment Integration Tests"
@echo ""
@echo "Usage: make -C tests <target> [USER=username]"
@echo ""
@echo "Targets:"
@echo " env - Test learner environment (SSH, tokens, Python)"
@echo " slack-bolt - Test slack-bolt Socket Mode connection"
@echo " vscode - Test VS Code Remote-SSH compatibility"
@echo " all - Run all tests"
@echo ""
@echo "Examples:"
@echo " make -C tests env"
@echo " make -C tests env USER=alice"
@echo " make -C tests all"
all: env slack-bolt
env:
@./test-learner-env.sh $(USER)
slack-bolt:
@echo "Testing slack-bolt on server as $(USER)..."
@ssh $(USER)@$(SERVER) 'source /etc/slack-learner.env && \
uv pip install --quiet slack-bolt 2>/dev/null && \
python3 -' < test-slack-bolt.py
vscode:
@echo "Testing VS Code Server binary compatibility..."
@echo "This test downloads and runs the VS Code Server binary."
@echo ""
@ssh $(USER)@$(SERVER) 'set -e; \
VSCODE_TEST_DIR=$$(mktemp -d); \
cd $$VSCODE_TEST_DIR; \
echo "Downloading VS Code Server CLI..."; \
curl -sL "https://code.visualstudio.com/sha/download?build=stable&os=cli-alpine-x64" -o vscode-cli.tar.gz; \
tar -xzf vscode-cli.tar.gz; \
echo "Testing binary execution..."; \
if ./code --version >/dev/null 2>&1; then \
echo "PASS: VS Code CLI executes (nix-ld working)"; \
./code --version | head -1; \
else \
echo "FAIL: VS Code CLI failed to execute"; \
echo " nix-ld may not be configured correctly"; \
exit 1; \
fi; \
rm -rf $$VSCODE_TEST_DIR'

197
tests/test-learner-env.sh Executable file
View file

@ -0,0 +1,197 @@
#!/usr/bin/env bash
# test-learner-env.sh - Integration tests for learner dev environment
# Run from local machine: ./tests/test-learner-env.sh [username] [ssh-key]
# Default username: dantest
set -euo pipefail
SERVER="45.77.205.49"
USER="${1:-dantest}"
SSH_KEY="${2:-}"
# Build SSH options
SSH_OPTS="-o ConnectTimeout=5"
if [ -n "$SSH_KEY" ]; then
SSH_OPTS="$SSH_OPTS -i $SSH_KEY"
fi
ssh_cmd() {
ssh $SSH_OPTS "${USER}@${SERVER}" "$@"
}
PASS=0
FAIL=0
red() { echo -e "\033[0;31m$1\033[0m"; }
green() { echo -e "\033[0;32m$1\033[0m"; }
yellow() { echo -e "\033[1;33m$1\033[0m"; }
pass() { PASS=$((PASS + 1)); green " PASS: $1"; }
fail() { FAIL=$((FAIL + 1)); red " FAIL: $1"; }
skip() { yellow " SKIP: $1"; }
echo "=========================================="
echo "Learner Environment Tests"
echo "Server: $SERVER"
echo "User: $USER"
echo "=========================================="
echo ""
# ---------------------------------------------
echo "## 1. SSH Connectivity"
# ---------------------------------------------
if ssh_cmd 'echo ok' &>/dev/null; then
pass "SSH connection as $USER"
else
fail "SSH connection as $USER"
echo " Hint: Check SSH key is configured for $USER"
echo " Usage: $0 <user> [/path/to/key]"
exit 1
fi
# ---------------------------------------------
echo ""
echo "## 2. nix-ld (VS Code Remote-SSH support)"
# ---------------------------------------------
if ssh_cmd 'test -L /lib64/ld-linux-x86-64.so.2' &>/dev/null; then
pass "nix-ld symlink exists"
else
fail "nix-ld symlink missing"
fi
# Test with a simple pre-compiled binary (curl is dynamically linked)
if ssh_cmd 'file /run/current-system/sw/bin/curl | grep -q "dynamically linked"' &>/dev/null; then
pass "Dynamic binaries available"
else
skip "Could not verify dynamic binary support"
fi
# ---------------------------------------------
echo ""
echo "## 3. Learners group membership"
# ---------------------------------------------
if ssh_cmd 'groups' | grep -q learners; then
pass "User in learners group"
else
fail "User NOT in learners group"
fi
# ---------------------------------------------
echo ""
echo "## 4. Slack tokens accessible"
# ---------------------------------------------
if ssh_cmd 'test -r /etc/slack-learner.env'; then
pass "Slack env file readable"
else
fail "Slack env file NOT readable (check group permissions)"
fi
if ssh_cmd 'source /etc/slack-learner.env && test -n "$SLACK_BOT_TOKEN"' &>/dev/null; then
pass "SLACK_BOT_TOKEN set"
else
fail "SLACK_BOT_TOKEN not set"
fi
if ssh_cmd 'source /etc/slack-learner.env && test -n "$SLACK_APP_TOKEN"' &>/dev/null; then
pass "SLACK_APP_TOKEN set"
else
fail "SLACK_APP_TOKEN not set"
fi
# ---------------------------------------------
echo ""
echo "## 5. Python environment"
# ---------------------------------------------
if ssh_cmd 'which python3' &>/dev/null; then
pass "python3 available"
PY_VERSION=$(ssh_cmd 'python3 --version 2>&1')
echo " $PY_VERSION"
else
fail "python3 not found"
fi
if ssh_cmd 'uv --version' &>/dev/null; then
UV_VERSION=$(ssh_cmd 'uv --version 2>&1')
pass "uv available ($UV_VERSION)"
else
fail "uv not available"
fi
# ---------------------------------------------
echo ""
echo "## 6. Home directory structure"
# ---------------------------------------------
if ssh_cmd 'test -d ~/plugins'; then
pass "~/plugins directory exists"
else
fail "~/plugins directory missing"
fi
if ssh_cmd 'test -d ~/plugins/hello-*' &>/dev/null; then
pass "Starter plugin exists"
else
skip "No starter plugin (may be expected)"
fi
# ---------------------------------------------
echo ""
echo "## 7. Slack API connectivity"
# ---------------------------------------------
echo " Testing Slack API auth (this may take a moment)..."
SLACK_TEST=$(ssh_cmd 'source /etc/slack-learner.env && python3 -c "
import urllib.request
import urllib.error
import json
import os
token = os.environ.get(\"SLACK_BOT_TOKEN\", \"\")
if not token:
print(\"NO_TOKEN\")
exit(1)
req = urllib.request.Request(
\"https://slack.com/api/auth.test\",
headers={\"Authorization\": f\"Bearer {token}\"}
)
try:
with urllib.request.urlopen(req, timeout=10) as resp:
data = json.loads(resp.read())
if data.get(\"ok\"):
user = data.get(\"user\", \"unknown\")
team = data.get(\"team\", \"unknown\")
print(f\"OK:{user}@{team}\")
else:
err = data.get(\"error\", \"unknown\")
print(f\"API_ERROR:{err}\")
except Exception as e:
print(f\"NET_ERROR:{e}\")
" 2>&1' || echo "EXEC_ERROR")
case "$SLACK_TEST" in
OK:*)
pass "Slack API auth successful"
echo " Authenticated as: ${SLACK_TEST#OK:}"
;;
API_ERROR:*)
fail "Slack API error: ${SLACK_TEST#API_ERROR:}"
;;
NET_ERROR:*)
fail "Network error: ${SLACK_TEST#NET_ERROR:}"
;;
NO_TOKEN)
fail "No SLACK_BOT_TOKEN available"
;;
*)
fail "Unexpected result: $SLACK_TEST"
;;
esac
# ---------------------------------------------
echo ""
echo "=========================================="
echo "Results: $PASS passed, $FAIL failed"
echo "=========================================="
if [ "$FAIL" -gt 0 ]; then
exit 1
fi

94
tests/test-slack-bolt.py Executable file
View file

@ -0,0 +1,94 @@
#!/usr/bin/env python3
"""Test slack-bolt can connect via Socket Mode.
Run on server as learner user:
python3 tests/test-slack-bolt.py
Requires: pip install slack-bolt
Expects: SLACK_BOT_TOKEN and SLACK_APP_TOKEN in environment
"""
import os
import sys
import time
import threading
def main():
# Check tokens
bot_token = os.environ.get("SLACK_BOT_TOKEN")
app_token = os.environ.get("SLACK_APP_TOKEN")
if not bot_token:
print("FAIL: SLACK_BOT_TOKEN not set")
print(" Hint: source /etc/slack-learner.env")
sys.exit(1)
if not app_token:
print("FAIL: SLACK_APP_TOKEN not set")
print(" Hint: source /etc/slack-learner.env")
sys.exit(1)
print(f"Bot token: {bot_token[:10]}...{bot_token[-4:]}")
print(f"App token: {app_token[:10]}...{app_token[-4:]}")
# Try importing slack-bolt
try:
from slack_bolt import App
from slack_bolt.adapter.socket_mode import SocketModeHandler
print("PASS: slack-bolt imported")
except ImportError as e:
print(f"FAIL: Cannot import slack-bolt: {e}")
print(" Hint: pip install slack-bolt")
sys.exit(1)
# Create app
print("Creating Slack app...")
app = App(token=bot_token)
# Track connection
connected = threading.Event()
error_msg = None
@app.event("app_home_opened")
def handle_home(event, logger):
# This won't fire in test, just proves handler registration works
pass
print("Starting Socket Mode connection (5 second test)...")
def run_socket():
nonlocal error_msg
try:
handler = SocketModeHandler(app, app_token)
# Start in background, will connect
handler.connect()
connected.set()
time.sleep(5)
handler.close()
except Exception as e:
error_msg = str(e)
connected.set()
thread = threading.Thread(target=run_socket)
thread.start()
# Wait for connection or timeout
connected.wait(timeout=10)
if error_msg:
print(f"FAIL: Socket Mode error: {error_msg}")
sys.exit(1)
if connected.is_set():
print("PASS: Socket Mode connected successfully")
print("")
print("All tests passed. Bot is ready for development.")
thread.join(timeout=2)
sys.exit(0)
else:
print("FAIL: Socket Mode connection timed out")
sys.exit(1)
if __name__ == "__main__":
main()