From cf0e610bc132dba364a052d2e9f3a5e270897b01 Mon Sep 17 00:00:00 2001 From: James Maes Date: Sun, 1 Nov 2026 05:08:19 -0600 Subject: [PATCH] feat(dev): add a local enterprise test server and a CLI sweep of its qbits scripts/enterprise-server.sh builds and runs qqq-all's enterprise profile (branch feat/enterprise-terminal-test in .worktrees/qqq-all-enterprise) on port 8078 against the UI API 1.9.0 fix-branch build: MOCK auth, every locally runnable qbit, the qqq sample app, Redis and OpenSearch in Docker for redis and quick search. --status and --stop only touch the pid it started and the two containers it names; it never touches the 8077 sample server. scripts/enterprise-smoke.sh runs the CLI through each qbit surface (ESB, alerts, assistant, collaboration, data analysis, DB performance, QA capture, quick search, release notes, secured data, SQL workbench, jobs, audits, saved views and reports, exports, webhooks, processes) and records exit codes, so client and server gaps show as non-zero lines. Co-Authored-By: Claude Fable 5.1 --- scripts/enterprise-server.sh | 123 +++++++++++++++++++++++++++++++++++ scripts/enterprise-smoke.sh | 109 +++++++++++++++++++++++++++++++ 2 files changed, 232 insertions(+) create mode 100755 scripts/enterprise-server.sh create mode 100755 scripts/enterprise-smoke.sh diff --git a/scripts/enterprise-server.sh b/scripts/enterprise-server.sh new file mode 100755 index 0000000..7c8b0a5 --- /dev/null +++ b/scripts/enterprise-server.sh @@ -0,0 +1,123 @@ +#!/usr/bin/env bash +# Start, stop or inspect the local "enterprise" QQQ test server: qqq-all's enterprise profile +# (every locally runnable qbit + the qqq sample app) on port 8078, MOCK auth (any user, admin). +# +# enterprise-server.sh [--start] | --status | --stop | --build | --log +# +# It only ever signals the pid it started (pid file in the state dir) and the two Docker +# containers it names (qqq-enterprise-redis, qqq-enterprise-opensearch). It never touches the +# 8077 sample server. +# +# Env: +# QQQ_ALL_ENTERPRISE_DIR qqq-all checkout on feat/enterprise-terminal-test +# (default ../.worktrees/qqq-all-enterprise next to this repo's parent) +# QQQ_ENTERPRISE_M2 Maven local repo holding the qqq build to run against +# (default ../.worktrees/qqq-fixes-m2: UI API 1.9.0 fix branch) +# QQQ_SAMPLE_CLASSES compiled qqq-sample-project classes to merge in +# (default ../.worktrees/qqq-fixes/qqq-sample-project/target/classes; +# missing = the sample app is left out) +# QQQ_ENTERPRISE_STATE pid, log and data directory (default ${TMPDIR:-/tmp}/qqq-enterprise-8078) +# QQQ_ENTERPRISE_PORT HTTP port (default 8078) +# QQQ_ENTERPRISE_DOCKER 0 = do not start Redis/OpenSearch containers (quick search and redis are +# then left out of the instance) +# QQQ_ALL_ENTERPRISE_EXCLUDE comma-separated feature names to leave out (see EnterpriseFeatures) +set -euo pipefail + +self="${BASH_SOURCE[0]}"; while [[ -L "$self" ]]; do self="$(readlink "$self")"; done +here="$(cd "$(dirname "$self")" && pwd)" +repo_root="$(cd "$here/.." && pwd)" +# worktrees live in /.worktrees; this repo is either /qqq-cli or /.worktrees/ +qrun_root="$(cd "$repo_root/.." && pwd)"; [[ "$(basename "$qrun_root")" == ".worktrees" ]] && qrun_root="$(cd "$qrun_root/.." && pwd)" +qqq_all="${QQQ_ALL_ENTERPRISE_DIR:-$qrun_root/.worktrees/qqq-all-enterprise}" +m2="${QQQ_ENTERPRISE_M2:-$qrun_root/.worktrees/qqq-fixes-m2}" +sample_classes="${QQQ_SAMPLE_CLASSES:-$qrun_root/.worktrees/qqq-fixes/qqq-sample-project/target/classes}" +port="${QQQ_ENTERPRISE_PORT:-8078}" +tmp_root="${TMPDIR:-/tmp}"; state_dir="${QQQ_ENTERPRISE_STATE:-${tmp_root%/}/qqq-enterprise-$port}" +state_dir="${state_dir%/}" +pid_file="$state_dir/server.pid" +log_file="$state_dir/server.log" +data_dir="$state_dir/data" +jar="$qqq_all/qqq-all-app/target/qqq-all-app.jar" +redis_name="qqq-enterprise-redis"; redis_port=6390 +search_name="qqq-enterprise-opensearch"; search_port=9210 +mkdir -p "$state_dir" + +running() { [[ -f "$pid_file" ]] && kill -0 "$(cat "$pid_file")" 2>/dev/null; } +use_docker() { [[ "${QQQ_ENTERPRISE_DOCKER:-1}" != "0" ]] && command -v docker > /dev/null && docker info > /dev/null 2>&1; } + +container_up() # name image port container-port [docker args...] +{ + local name="$1" image="$2" host_port="$3" container_port="$4"; shift 4 + if [[ "$(docker inspect -f '{{.State.Running}}' "$name" 2>/dev/null)" == "true" ]]; then return 0; fi + if docker inspect "$name" > /dev/null 2>&1; then docker start "$name" > /dev/null; return 0; fi + docker run -d --name "$name" -p "127.0.0.1:$host_port:$container_port" "$@" "$image" > /dev/null +} + +build() +{ + [[ -f "$qqq_all/pom.xml" ]] || { echo "no qqq-all checkout at $qqq_all (set QQQ_ALL_ENTERPRISE_DIR)"; exit 1; } + echo "building qqq-all-app (enterprise profile) against $m2 …" + mvn -B -q -f "$qqq_all/pom.xml" -Dmaven.repo.local="$m2" -DskipTests -pl qqq-all-app -am package + echo "built $jar" +} + +case "${1:---start}" in + --build|build) build ;; + --start|start) + if running; then echo "already running (pid $(cat "$pid_file")) on http://localhost:$port"; exit 0; fi + if curl -sf -m 2 "http://localhost:$port/qqq/v1/version" > /dev/null 2>&1; then + echo "port $port is already serving something that this script did not start; not starting"; exit 1 + fi + [[ -f "$jar" ]] || build + env_args=(QQQ_ALL_PROFILE=enterprise QQQ_ALL_PORT="$port" QQQ_ALL_DATA_DIR="$data_dir") + if use_docker; then + echo "starting Redis ($redis_name, :$redis_port) and OpenSearch ($search_name, :$search_port) containers" + container_up "$redis_name" redis:7.4-alpine "$redis_port" 6379 + container_up "$search_name" opensearchproject/opensearch:2.19.1 "$search_port" 9200 \ + -e discovery.type=single-node -e DISABLE_SECURITY_PLUGIN=true -e "OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m" + for _ in $(seq 1 60); do curl -sf -m 2 "http://127.0.0.1:$search_port/_cluster/health" > /dev/null 2>&1 && break; sleep 2; done + env_args+=(QQQ_ALL_OPENSEARCH_HOST=127.0.0.1 QQQ_ALL_OPENSEARCH_PORT="$search_port" + QQQ_ALL_REDIS_HOST=127.0.0.1 QQQ_ALL_REDIS_PORT="$redis_port") + else + echo "Docker not used: quick search and redis are left out" + fi + classpath="$jar" + if [[ -d "$sample_classes" ]]; then classpath="$jar:$sample_classes"; else echo "no sample classes at $sample_classes: sample app left out"; fi + echo "starting enterprise server on port $port (log: $log_file, data: $data_dir)" + nohup env "${env_args[@]}" java -cp "$classpath" com.kingsrook.qqq.all.ProfileLauncher > "$log_file" 2>&1 & + echo $! > "$pid_file" + for _ in $(seq 1 90); do + if curl -sf -m 2 "http://localhost:$port/qqq/v1/version" > /dev/null 2>&1; then + echo "ready: http://localhost:$port (UI API $(curl -s "http://localhost:$port/qqq/v1/version" | sed 's/.*"apiVersion":"\([^"]*\)".*/\1/'))" + echo "login: qqq login --server http://localhost:$port (mock auth: any user is an all-access admin)" + exit 0 + fi + if ! running; then echo "server exited early; see $log_file"; tail -20 "$log_file"; rm -f "$pid_file"; exit 1; fi + sleep 2 + done + echo "timed out waiting for the server; see $log_file"; exit 1 + ;; + --stop|stop) + if running; then + kill "$(cat "$pid_file")" 2>/dev/null || true + for _ in $(seq 1 15); do running || break; sleep 1; done + running && kill -9 "$(cat "$pid_file")" 2>/dev/null || true + echo "stopped pid $(cat "$pid_file")" + else + echo "not running (no live pid in $pid_file)" + fi + rm -f "$pid_file" + if use_docker; then docker stop "$redis_name" "$search_name" > /dev/null 2>&1 && echo "stopped containers $redis_name $search_name" || true; fi + ;; + --status|status) + if running; then + echo "running: pid $(cat "$pid_file") on http://localhost:$port" + curl -s -m 3 "http://localhost:$port/qqq/v1/version" | python3 -c 'import json,sys; d=json.load(sys.stdin); print("UI API", d["apiVersion"]); print("extensions:", ", ".join(e["name"]+" "+e.get("version","?") for e in d.get("extensions",[])))' 2>/dev/null || echo "(not answering on /qqq/v1/version)" + else + echo "not running" + fi + echo "log: $log_file" + ;; + --log|log) tail -n 100 -f "$log_file" ;; + *) echo "usage: $0 [--start] | --status | --stop | --build | --log"; exit 2 ;; +esac diff --git a/scripts/enterprise-smoke.sh b/scripts/enterprise-smoke.sh new file mode 100755 index 0000000..265d13e --- /dev/null +++ b/scripts/enterprise-smoke.sh @@ -0,0 +1,109 @@ +#!/usr/bin/env bash +# CLI sweep of every qbit surface on the enterprise test server (scripts/enterprise-server.sh). +# Runs each command, records its exit code and the first lines of output; non-zero lines are the gaps. +# scripts/enterprise-smoke.sh QQQ="qqq --profile enterprise" OUT=file N=8 +# Writes a few records (an audit probe customer, a comment, a follow, a saved view) on the test server. +here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +QQQ_CMD=(${QQQ:-node $here/../packages/cli/dist/main.js}) +OUT=${OUT:-${TMPDIR:-/tmp}/qqq-enterprise-smoke.txt}; : > "$OUT" +DL=${DL:-$(mktemp -d)} +run() { local label="$1"; shift; local o; o=$("${QQQ_CMD[@]}" "$@" 2>&1); local rc=$?; { echo "### [$rc] $label :: qqq $*"; echo "$o" | head -${N:-8} | cut -c1-240; } >> "$OUT"; } +run whoami whoami +run "apps list" apps list -O text +run home home -O json +run "process list count" process list -O json -Q 'length(@)' +run "widget list count" widget list -O json -Q 'length(@)' +run "find customer" find customer +run "search (global)" search Chicago +run "doctor" doctor +# ESB +run "esb overview" esb overview +run "esb table order" esb table order +run "esb process syncOrder" esb process syncOrder +run "esb messages" esb messages orderEvents --trigger syncOrder.orderEvents +# health (not installed on 8078) +run "health summary" health get-summary +# alerts +run "alerts inbox" alerts get-inbox +run "alerts summary" alerts get-inbox-summary +run "alerts record panel" alerts get-record-panel -q tableName=customer recordId=1 +# assistant +run "assistant config" assistant get-config +run "assistant conversations" assistant get-conversations +run "assistant access" assistant get-access +# collaboration +run "collab presence table" collab presence customer +run "collab presence record" collab presence customer 1 +run "collab favorites" collab favorites list +run "collab recents" collab recents +run "collab people" collab people admin +run "collab my-work" collab my-work +run "collab comments list" collab comments list customer 1 +run "collab comments add" collab comments add customer 1 "enterprise smoke comment" +run "collab tags record" collab tags list customer 1 +run "collab follow" collab follow customer 1 +run "collab timeline" collab get-records-timeline customer 1 +run "collab attachments" collab get-records-attachments customer 1 +run "collab share preview" collab post-records-shares-preview customer 1 -b '{"toUserIds":["core:admin"],"message":"look"}' +# data analysis +run "da capabilities customer" data-analysis get-tables-capabilities customer +run "da table profile customer" data-analysis post-tables-table-profile customer -b '{}' +run "da column profile customer.name" data-analysis post-tables-column-profile customer -b '{"fieldName":"name"}' +run "da distinct" data-analysis post-tables-distinct-values order -b '{"fieldName":"status"}' +run "da saved" data-analysis get-saved +# db performance +run "dbperf overview" db-performance get-overview +run "dbperf activity" db-performance get-activity +# qa capture +run "qa capability" qa-capture get-capability +# quick search +run "qs status" quick-search get-status +run "qs suggest" quick-search get-suggest -q q=a +run "table query --search" table query customer --search a +# release notes +run "rn releases" release-notes get-releases +run "rn summary" release-notes get-summary +# secured data +run "sd status" secured-data get-status +run "sd reveal options" secured-data get-reveal-options -q tableName=customer fieldName=email +# sql workbench +run "sqlwb capabilities" sql-workbench get-capabilities +run "sqlwb schema h2" sql-workbench get-schema h2 +run "sqlwb execute" sql-workbench post-execute -b '{"backendName":"h2","sql":"select id, name from customer"}' +run "sqlwb history" sql-workbench get-history +run "sqlwb notebooks" sql-workbench get-notebooks +# jobs +run "job list" job list +run "job list --all" job list --all +# reports / saved reports / views / bulk profiles +run "report list" report list +run "saved-report list" saved-report list +run "saved-view list" saved-view list customer +run "bulk-profile list" bulk-profile list customer +# prefs, help +run "prefs list" prefs list +run "help-search" help-search order +# webhooks (tables) +run "webhook query" table query webhook +run "webhook subscription query" table query webhookSubscription +run "webhook actions" table actions webhook +# scripts +run "script tables" table query script --limit 3 +# exports +run "export customer csv" table export customer --format csv --out $DL/customer.csv +run "export order xlsx" table export order --format xlsx --out $DL/order.xlsx +run "export person json" table export person --format json --out $DL/person.json +run "qs reindex process" process run quickSearchFullReindex --yes +run "qs suggest after reindex" quick-search get-suggest -q q=ada +run "search after reindex" search ada +run "audit create" table create customer --set name="Audit Probe" --set email=probe@example.test +run "audit update" table update customer 1 --set email=ada2@example.test +run "audit list" audit list customer 1 +run "saved view save" saved-view save customer -n "Enterprise smoke" -w "name ~ a" --replace +run "saved view list" saved-view list customer +run "process run calculateOrderTotal" process run calculateOrderTotal -v quantity=3 unitPriceCents=1250 +run "process run greet" process run greet -r 1,2 --yes +run "process describe syncOrder" process describe syncOrder +run "webhook test event" process run SendTestEventToWebhook -r 1 --yes +run "widget get first" widget list -O json -Q '[0:5].name' +echo "done: $(grep -c "^### \[0\]" "$OUT") passed, $(grep -c "^### \[[1-9]" "$OUT") non-zero; $OUT"