From d387b0b91066ba47500dcb8ed7ea650381bf101d Mon Sep 17 00:00:00 2001 From: Dan Date: Fri, 2 Jan 2026 12:16:58 -0800 Subject: [PATCH] Add learner environment integration tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- configuration.nix | 3 + tests/Makefile | 55 +++++++++++ tests/test-learner-env.sh | 197 ++++++++++++++++++++++++++++++++++++++ tests/test-slack-bolt.py | 94 ++++++++++++++++++ 4 files changed, 349 insertions(+) create mode 100644 tests/Makefile create mode 100755 tests/test-learner-env.sh create mode 100755 tests/test-slack-bolt.py diff --git a/configuration.nix b/configuration.nix index 68bdf10..0334a71 100644 --- a/configuration.nix +++ b/configuration.nix @@ -24,6 +24,9 @@ git htop curl + # Python for learner bot development + python3 + uv ]; # Enable Nix flakes diff --git a/tests/Makefile b/tests/Makefile new file mode 100644 index 0000000..adbb662 --- /dev/null +++ b/tests/Makefile @@ -0,0 +1,55 @@ +# Integration tests for learner dev environment +# Run from repo root: make -C tests + +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 [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' diff --git a/tests/test-learner-env.sh b/tests/test-learner-env.sh new file mode 100755 index 0000000..9b21ac3 --- /dev/null +++ b/tests/test-learner-env.sh @@ -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 [/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 diff --git a/tests/test-slack-bolt.py b/tests/test-slack-bolt.py new file mode 100755 index 0000000..ccfa7cf --- /dev/null +++ b/tests/test-slack-bolt.py @@ -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()