#!/usr/bin/env bash set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" COMMON_PLAT_DIR="${COMMON_PLAT_DIR:-${ROOT_DIR}/../learning_ai_common_plat}" if [[ ! -d "$COMMON_PLAT_DIR" && -d "${ROOT_DIR}/../learning_ai/learning_ai_common_plat" ]]; then COMMON_PLAT_DIR="${ROOT_DIR}/../learning_ai/learning_ai_common_plat" fi NOTELETT_URL="${NOTELETT_URL:-http://localhost:4016}" NOTELETT_API_URL="${NOTELETT_API_URL:-${NOTELETT_URL}/api}" PLATFORM_URL="${PLATFORM_URL:-http://localhost:4003}" EXTRACTION_URL="${EXTRACTION_URL:-http://localhost:4005}" MCP_ORIGIN="${MCP_ORIGIN:-http://localhost:4007}" JWT_SECRET="${JWT_SECRET:-dev-secret-do-not-use-in-prod}" PRODUCT_ID="${PRODUCT_ID:-notelett}" SMOKE_USER_ID="${SMOKE_USER_ID:-notelett-smoke-user}" SMOKE_START_BACKEND="${SMOKE_START_BACKEND:-1}" SMOKE_SKIP_PLATFORM="${SMOKE_SKIP_PLATFORM:-0}" BACKEND_PID="" BACKEND_LOG="${TMPDIR:-/tmp}/notelett-local-smoke-backend.log" usage() { cat <<'USAGE' Usage: scripts/local-smoke.sh [--no-start] [--skip-platform] [--help] Checks: - NoteLett GET /health - NoteLett GET /api/bootstrap - platform-service, extraction-service, and mcp-server /health - authenticated workspace + note create/read/delete flow in DB_PROVIDER=memory Environment: NOTELETT_URL=http://localhost:4016 PLATFORM_URL=http://localhost:4003 EXTRACTION_URL=http://localhost:4005 MCP_ORIGIN=http://localhost:4007 JWT_SECRET=dev-secret-do-not-use-in-prod SMOKE_START_BACKEND=1 SMOKE_SKIP_PLATFORM=0 COMMON_PLAT_DIR=../learning_ai/learning_ai_common_plat USAGE } for arg in "$@"; do case "$arg" in --) ;; --no-start) SMOKE_START_BACKEND=0 ;; --skip-platform) SMOKE_SKIP_PLATFORM=1 ;; --help|-h) usage exit 0 ;; *) echo "Unknown argument: $arg" >&2 usage >&2 exit 2 ;; esac done cleanup() { if [[ -n "$BACKEND_PID" ]] && kill -0 "$BACKEND_PID" >/dev/null 2>&1; then kill "$BACKEND_PID" >/dev/null 2>&1 || true wait "$BACKEND_PID" >/dev/null 2>&1 || true fi } trap cleanup EXIT curl_json() { local method="$1" local url="$2" local body="${3:-}" shift 3 || true if [[ -n "$body" ]]; then curl -fsS -X "$method" "$url" \ -H 'content-type: application/json' \ "$@" \ --data "$body" else curl -fsS -X "$method" "$url" "$@" fi } healthy() { curl -fsS "$NOTELETT_URL/health" >/dev/null 2>&1 } ensure_common_platform_builds() { if [[ ! -d "$COMMON_PLAT_DIR" ]]; then echo "error: missing common-platform checkout at $COMMON_PLAT_DIR" >&2 return 1 fi local palace_dist="$COMMON_PLAT_DIR/packages/palace/dist/index.js" if [[ ! -f "$palace_dist" ]]; then echo "info: building linked common-platform @bytelyst/palace package" GITEA_NPM_TOKEN="${GITEA_NPM_TOKEN:-dummy}" \ pnpm -C "$COMMON_PLAT_DIR" --filter @bytelyst/palace run build fi local llm_dist="$COMMON_PLAT_DIR/packages/llm/dist/index.js" if [[ ! -f "$llm_dist" ]] || ! grep -q 'buildVisionMessage' "$llm_dist"; then echo "info: building linked common-platform @bytelyst/llm package" GITEA_NPM_TOKEN="${GITEA_NPM_TOKEN:-dummy}" \ pnpm -C "$COMMON_PLAT_DIR" --filter @bytelyst/llm run build fi } start_backend_if_needed() { if healthy; then echo "ok: NoteLett backend already healthy at $NOTELETT_URL" return fi if [[ "$SMOKE_START_BACKEND" != "1" ]]; then echo "error: NoteLett backend is not healthy at $NOTELETT_URL and SMOKE_START_BACKEND=0" >&2 return 1 fi echo "info: starting NoteLett backend in memory mode" ensure_common_platform_builds ( cd "$ROOT_DIR" DB_PROVIDER=memory \ NODE_ENV=development \ JWT_SECRET="$JWT_SECRET" \ PRODUCT_ID="$PRODUCT_ID" \ pnpm --filter @notelett/backend run dev ) >"$BACKEND_LOG" 2>&1 & BACKEND_PID="$!" for _ in {1..60}; do if healthy; then echo "ok: NoteLett backend started at $NOTELETT_URL" return fi if ! kill -0 "$BACKEND_PID" >/dev/null 2>&1; then echo "error: backend exited before becoming healthy; log follows" >&2 tail -n 80 "$BACKEND_LOG" >&2 || true return 1 fi sleep 1 done echo "error: backend did not become healthy within 60 seconds; log follows" >&2 tail -n 80 "$BACKEND_LOG" >&2 || true return 1 } generate_token() { ( cd "$ROOT_DIR/backend" JWT_SECRET="$JWT_SECRET" PRODUCT_ID="$PRODUCT_ID" SMOKE_USER_ID="$SMOKE_USER_ID" node --input-type=module <<'NODE' import { SignJWT } from 'jose'; const secret = new TextEncoder().encode(process.env.JWT_SECRET); const productId = process.env.PRODUCT_ID ?? 'notelett'; const sub = process.env.SMOKE_USER_ID ?? 'notelett-smoke-user'; const token = await new SignJWT({ email: 'smoke@notelett.local', role: 'admin', productId, type: 'access', }) .setProtectedHeader({ alg: 'HS256' }) .setSubject(sub) .setIssuer('bytelyst-platform') .setIssuedAt() .setExpirationTime('15m') .sign(secret); process.stdout.write(token); NODE ) } json_field_equals() { local json="$1" local field="$2" local expected="$3" JSON_INPUT="$json" FIELD="$field" EXPECTED="$expected" node --input-type=module <<'NODE' const body = JSON.parse(process.env.JSON_INPUT ?? '{}'); const actual = body[process.env.FIELD]; if (actual !== process.env.EXPECTED) { console.error(`Expected ${process.env.FIELD}=${process.env.EXPECTED}, got ${String(actual)}`); process.exit(1); } NODE } check_platform_dependencies() { if [[ "$SMOKE_SKIP_PLATFORM" == "1" ]]; then echo "skip: platform dependency checks disabled" return fi curl -fsS "$PLATFORM_URL/health" >/dev/null echo "ok: platform-service health" curl -fsS "$EXTRACTION_URL/health" >/dev/null echo "ok: extraction-service health" curl -fsS "$MCP_ORIGIN/health" >/dev/null echo "ok: mcp-server health" } run_product_flow() { local token="$1" local run_id run_id="smoke-$(date +%s)-$$" local workspace_id="ws-$run_id" local note_id="note-$run_id" local auth_header="authorization: Bearer $token" local workspace_body workspace_body="{\"id\":\"$workspace_id\",\"name\":\"Smoke Workspace\",\"members\":[]}" local workspace workspace="$(curl_json POST "$NOTELETT_API_URL/workspaces" "$workspace_body" -H "$auth_header")" json_field_equals "$workspace" id "$workspace_id" echo "ok: authenticated workspace create" local note_body note_body="{\"id\":\"$note_id\",\"workspaceId\":\"$workspace_id\",\"title\":\"Smoke Note\",\"body\":\"

Local smoke note body.

\",\"tags\":[\"smoke\"],\"links\":[]}" local note note="$(curl_json POST "$NOTELETT_API_URL/notes" "$note_body" -H "$auth_header")" json_field_equals "$note" id "$note_id" echo "ok: authenticated note create" local fetched fetched="$(curl_json GET "$NOTELETT_API_URL/notes/$note_id?workspaceId=$workspace_id" "" -H "$auth_header")" json_field_equals "$fetched" productId "$PRODUCT_ID" json_field_equals "$fetched" id "$note_id" echo "ok: authenticated note read" curl_json DELETE "$NOTELETT_API_URL/notes/$note_id?workspaceId=$workspace_id" "" -H "$auth_header" >/dev/null 2>&1 || true curl_json DELETE "$NOTELETT_API_URL/workspaces/$workspace_id" "" -H "$auth_header" >/dev/null 2>&1 || true echo "ok: smoke cleanup attempted" } main() { start_backend_if_needed curl -fsS "$NOTELETT_URL/health" >/dev/null echo "ok: NoteLett health" local bootstrap bootstrap="$(curl_json GET "$NOTELETT_API_URL/bootstrap" "")" json_field_equals "$bootstrap" productId "$PRODUCT_ID" echo "ok: NoteLett bootstrap" check_platform_dependencies local token token="$(generate_token)" run_product_flow "$token" echo "ok: local production-readiness smoke passed" } main