From fc56fdaed1372c6db2ba4be2af14f8501a060632 Mon Sep 17 00:00:00 2001 From: imbajin Date: Tue, 21 Jul 2026 23:25:28 +0800 Subject: [PATCH 1/2] feat: add Codex Hubble development environment --- .codex/README.md | 28 ++ .codex/environments/environment.toml | 73 +++++ .codex/infra/compose.balanced.yml | 75 +++++ .codex/infra/compose.low-memory.yml | 90 ++++++ .codex/infra/patch-server-config.sh | 51 +++ .codex/scripts/hubble.sh | 457 +++++++++++++++++++++++++++ .codex/scripts/infra.sh | 174 ++++++++++ .codex/scripts/setup.sh | 92 ++++++ .codex/scripts/verify.sh | 67 ++++ .codex/tests/environment_test.sh | 200 ++++++++++++ hugegraph-hubble/README.md | 7 + 11 files changed, 1314 insertions(+) create mode 100644 .codex/README.md create mode 100644 .codex/environments/environment.toml create mode 100644 .codex/infra/compose.balanced.yml create mode 100644 .codex/infra/compose.low-memory.yml create mode 100755 .codex/infra/patch-server-config.sh create mode 100755 .codex/scripts/hubble.sh create mode 100755 .codex/scripts/infra.sh create mode 100755 .codex/scripts/setup.sh create mode 100755 .codex/scripts/verify.sh create mode 100755 .codex/tests/environment_test.sh diff --git a/.codex/README.md b/.codex/README.md new file mode 100644 index 000000000..601de5538 --- /dev/null +++ b/.codex/README.md @@ -0,0 +1,28 @@ +# Toolchain local environment + +Codex reads `.codex/environments/environment.toml` when it creates a worktree. +The setup is intentionally lightweight: it pins Node 18.20.8 and installs the +frontend dependencies without building or starting services. + +## Runtime model + +- Each worktree gets a stable frontend port in `3001-3099`. +- Up to three frontend dev servers are recommended; additional servers warn. +- One Hubble backend listens on `8088`. The first worktree owns it until the + `Hubble BE Switch` action explicitly replaces it. +- HStore uses the sibling server repository's Compose file plus the tracked + low-memory override. Images use the local `latest` tag and never pull during + startup. `Infrastructure Pull` is the explicit update operation. +- `Infrastructure Stop` preserves HStore data. `Infrastructure Reset` requires + typing `RESET` before it deletes the Compose volumes. +- Runtime ownership files live under `~/.codex/run/`, not in a worktree. + +Set `HUGEGRAPH_SERVER_REPO` when the server repository is not next to +toolchain. Set `HUBBLE_LOADER_HOME` when no packaged Loader exists under the +source checkout. Set `HUBBLE_MVND_BIN` to override the Java 11-compatible mvnd +binary. Secrets must stay in local environment variables or an untracked local +file; do not add them to the environment TOML. + +The low-memory HStore profile is intended only for local, small-data functional +testing. Use the balanced profile for larger Loader samples. Neither Docker +Desktop nor these constrained profiles are valid performance baselines. diff --git a/.codex/environments/environment.toml b/.codex/environments/environment.toml new file mode 100644 index 000000000..460df2ff0 --- /dev/null +++ b/.codex/environments/environment.toml @@ -0,0 +1,73 @@ +version = 1 +name = "toolchain-hubble" + +[setup] +script = "bash .codex/scripts/setup.sh" + +[cleanup] +script = "bash .codex/scripts/hubble.sh cleanup" + +[[actions]] +name = "Hubble FE Dev" +icon = "run" +command = "bash .codex/scripts/hubble.sh fe-start" + +[[actions]] +name = "Hubble BE Dev" +icon = "run" +command = "bash .codex/scripts/hubble.sh be-start" + +[[actions]] +name = "Hubble BE Switch" +icon = "debug" +command = "bash .codex/scripts/hubble.sh be-switch" + +[[actions]] +name = "Hubble Status" +icon = "tool" +command = "bash .codex/scripts/hubble.sh status" + +[[actions]] +name = "Hubble Stop" +icon = "tool" +command = "bash .codex/scripts/hubble.sh stop" + +[[actions]] +name = "HStore Low Memory" +icon = "run" +command = "bash .codex/scripts/infra.sh hstore-start low" + +[[actions]] +name = "HStore Balanced" +icon = "run" +command = "bash .codex/scripts/infra.sh hstore-start balanced" + +[[actions]] +name = "Infrastructure Status" +icon = "tool" +command = "bash .codex/scripts/infra.sh status" + +[[actions]] +name = "Infrastructure Stop" +icon = "tool" +command = "bash .codex/scripts/infra.sh stop" + +[[actions]] +name = "Infrastructure Reset" +icon = "tool" +command = "bash .codex/scripts/infra.sh reset" + +[[actions]] +name = "Infrastructure Pull" +icon = "tool" +command = "bash .codex/scripts/infra.sh pull" + +[[actions]] +name = "Fast Verify" +icon = "test" +command = "bash .codex/scripts/verify.sh fast" + +[[actions]] +name = "Full Verify" +icon = "test" +command = "bash .codex/scripts/verify.sh full" diff --git a/.codex/infra/compose.balanced.yml b/.codex/infra/compose.balanced.yml new file mode 100644 index 000000000..5ead46160 --- /dev/null +++ b/.codex/infra/compose.balanced.yml @@ -0,0 +1,75 @@ +services: + pd: + mem_limit: 640m + environment: + JAVA_OPTS: >- + -Xms96m -Xmx128m + -XX:ActiveProcessorCount=2 + -XX:MaxMetaspaceSize=128m + -XX:MaxDirectMemorySize=96m + -XX:+UseContainerSupport + -Dlog4j2.asyncLoggerConfigRingBufferSize=8192 + -DAsyncLoggerConfig.RingBufferSize=8192 + -Dthread.pool.grpc.core=8 + -Dthread.pool.grpc.max=32 + -Dthread.pool.grpc.queue=512 + -Djob.interruptableThreadPool.core=1 + -Djob.interruptableThreadPool.max=8 + -Djob.interruptableThreadPool.queue=256 + -Djob.uninterruptibleThreadPool.core=0 + -Djob.uninterruptibleThreadPool.max=4 + -Djob.uninterruptibleThreadPool.queue=128 + -Dpartition.default-shard-count=1 + -Dpartition.store-max-shard-count=12 + + store: + mem_limit: 768m + environment: + JAVA_OPTS: >- + -Xms96m -Xmx128m + -XX:ActiveProcessorCount=2 + -XX:MaxMetaspaceSize=128m + -XX:MaxDirectMemorySize=128m + -XX:+UseContainerSupport + -Dlog4j2.asyncLoggerConfigRingBufferSize=8192 + -DAsyncLoggerConfig.RingBufferSize=8192 + -Drocksdb.total_memory_size=134217728 + -Drocksdb.write_buffer_size=4194304 + -Drocksdb.min_write_buffer_number_to_merge=2 + -Dthread.pool.grpc.core=8 + -Dthread.pool.grpc.max=32 + -Dthread.pool.grpc.queue=512 + -Dthread.pool.scan.core=4 + -Dthread.pool.scan.max=16 + -Dthread.pool.scan.queue=256 + -Djob.interruptableThreadPool.core=2 + -Djob.interruptableThreadPool.max=8 + -Djob.interruptableThreadPool.queue=256 + -Djob.uninterruptibleThreadPool.core=0 + -Djob.uninterruptibleThreadPool.max=4 + -Djob.uninterruptibleThreadPool.queue=128 + -Dquery.push-down.threads=8 + -Dquery.push-down.fetch_batch=1000 + -Dquery.push-down.fetch_timeout=30000 + -Dquery.push-down.memory_limit_count=10000 + -Dquery.push-down.index_size_limit_count=10000 + -Draft.disruptorBufferSize=512 + -Draft.metrics=false + -Draft.maxReplicatorInflightMsgs=32 + -Draft.maxEntriesSize=64 + -Draft.maxBodySize=262144 + + server: + mem_limit: 1024m + environment: + JAVA_OPTS: >- + -Xms128m -Xmx384m + -XX:ActiveProcessorCount=4 + -XX:MaxMetaspaceSize=192m + -XX:MaxDirectMemorySize=128m + -XX:+UseContainerSupport + -Dlog4j2.asyncLoggerConfigRingBufferSize=8192 + -DAsyncLoggerConfig.RingBufferSize=8192 + --add-exports=java.base/jdk.internal.reflect=ALL-UNNAMED + --add-modules=jdk.unsupported + --add-exports=java.base/sun.nio.ch=ALL-UNNAMED diff --git a/.codex/infra/compose.low-memory.yml b/.codex/infra/compose.low-memory.yml new file mode 100644 index 000000000..2e6fce1fd --- /dev/null +++ b/.codex/infra/compose.low-memory.yml @@ -0,0 +1,90 @@ +services: + pd: + pull_policy: never + mem_limit: 512m + cpus: 2.0 + environment: + JAVA_OPTS: >- + -Xms64m -Xmx64m + -XX:ActiveProcessorCount=2 + -XX:MaxMetaspaceSize=96m + -XX:MaxDirectMemorySize=64m + -XX:+UseContainerSupport + -Dlog4j2.asyncLoggerConfigRingBufferSize=4096 + -DAsyncLoggerConfig.RingBufferSize=4096 + -Dthread.pool.grpc.core=8 + -Dthread.pool.grpc.max=32 + -Dthread.pool.grpc.queue=256 + -Djob.interruptableThreadPool.core=1 + -Djob.interruptableThreadPool.max=8 + -Djob.interruptableThreadPool.queue=128 + -Djob.uninterruptibleThreadPool.core=0 + -Djob.uninterruptibleThreadPool.max=4 + -Djob.uninterruptibleThreadPool.queue=64 + -Dpartition.default-shard-count=1 + -Dpartition.store-max-shard-count=12 + + store: + pull_policy: never + mem_limit: 512m + cpus: 2.0 + environment: + JAVA_OPTS: >- + -Xms64m -Xmx64m + -XX:ActiveProcessorCount=2 + -XX:MaxMetaspaceSize=96m + -XX:MaxDirectMemorySize=64m + -XX:+UseContainerSupport + -Dlog4j2.asyncLoggerConfigRingBufferSize=4096 + -DAsyncLoggerConfig.RingBufferSize=4096 + -Drocksdb.total_memory_size=67108864 + -Drocksdb.write_buffer_size=1048576 + -Drocksdb.min_write_buffer_number_to_merge=2 + -Dthread.pool.grpc.core=8 + -Dthread.pool.grpc.max=32 + -Dthread.pool.grpc.queue=256 + -Dthread.pool.scan.core=4 + -Dthread.pool.scan.max=16 + -Dthread.pool.scan.queue=128 + -Djob.interruptableThreadPool.core=2 + -Djob.interruptableThreadPool.max=8 + -Djob.interruptableThreadPool.queue=128 + -Djob.uninterruptibleThreadPool.core=0 + -Djob.uninterruptibleThreadPool.max=4 + -Djob.uninterruptibleThreadPool.queue=64 + -Dquery.push-down.threads=8 + -Dquery.push-down.fetch_batch=1000 + -Dquery.push-down.fetch_timeout=30000 + -Dquery.push-down.memory_limit_count=5000 + -Dquery.push-down.index_size_limit_count=5000 + -Draft.disruptorBufferSize=256 + -Draft.metrics=false + -Draft.maxReplicatorInflightMsgs=16 + -Draft.maxEntriesSize=32 + -Draft.maxBodySize=131072 + + server: + pull_policy: never + mem_limit: 768m + cpus: 4.0 + entrypoint: + - /usr/bin/dumb-init + - "--" + - /opt/toolchain-low-memory/patch-server-config.sh + environment: + JAVA_OPTS: >- + -Xms128m -Xmx192m + -XX:ActiveProcessorCount=4 + -XX:MaxMetaspaceSize=128m + -XX:MaxDirectMemorySize=64m + -XX:+UseContainerSupport + -Dlog4j2.asyncLoggerConfigRingBufferSize=4096 + -DAsyncLoggerConfig.RingBufferSize=4096 + --add-exports=java.base/jdk.internal.reflect=ALL-UNNAMED + --add-modules=jdk.unsupported + --add-exports=java.base/sun.nio.ch=ALL-UNNAMED + volumes: + - type: bind + source: ${TOOLCHAIN_CODEX_DIR}/infra/patch-server-config.sh + target: /opt/toolchain-low-memory/patch-server-config.sh + read_only: true diff --git a/.codex/infra/patch-server-config.sh b/.codex/infra/patch-server-config.sh new file mode 100755 index 000000000..90514e6fb --- /dev/null +++ b/.codex/infra/patch-server-config.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash + +set -euo pipefail +unset CDPATH + +CONF_DIR=${HG_SERVER_CONF_DIR:-./conf} +REST_CONF="${CONF_DIR}/rest-server.properties" +GREMLIN_CONF="${CONF_DIR}/gremlin-server.yaml" + +set_property() { + local file=$1 key=$2 value=$3 escaped + escaped=${key//./\.} + if grep -qE "^[[:space:]]*${escaped}[[:space:]]*=" "${file}"; then + sed -E -i.bak \ + "s|^[[:space:]]*${escaped}[[:space:]]*=.*|${key}=${value}|" "${file}" + rm -f "${file}.bak" + else + printf '%s=%s\n' "${key}" "${value}" >> "${file}" + fi +} + +set_yaml_scalar() { + local file=$1 key=$2 value=$3 + grep -qE "^[[:space:]]*${key}:" "${file}" || { + echo "ERROR: missing YAML key '${key}' in ${file}" >&2 + exit 1 + } + sed -E -i.bak \ + "s|^([[:space:]]*)${key}:.*|\\1${key}: ${value}|" "${file}" + rm -f "${file}.bak" +} + +[[ -f "${REST_CONF}" ]] || { echo "ERROR: missing ${REST_CONF}" >&2; exit 1; } +[[ -f "${GREMLIN_CONF}" ]] || { echo "ERROR: missing ${GREMLIN_CONF}" >&2; exit 1; } + +set_property "${REST_CONF}" batch.max_write_threads \ + "${HG_SERVER_BATCH_WRITE_THREADS:-2}" +set_property "${REST_CONF}" restserver.min_free_memory \ + "${HG_SERVER_MIN_FREE_MEMORY:-16}" +set_yaml_scalar "${GREMLIN_CONF}" threadPoolWorker \ + "${HG_SERVER_GREMLIN_WORKERS:-2}" +set_yaml_scalar "${GREMLIN_CONF}" gremlinPool \ + "${HG_SERVER_GREMLIN_POOL:-2}" + +echo "[infra] applied low-memory Server configuration" + +if [[ "${1:-}" == "--patch-only" ]]; then + exit 0 +fi + +exec ./docker-entrypoint.sh diff --git a/.codex/scripts/hubble.sh b/.codex/scripts/hubble.sh new file mode 100755 index 000000000..0ee432f00 --- /dev/null +++ b/.codex/scripts/hubble.sh @@ -0,0 +1,457 @@ +#!/usr/bin/env bash + +set -euo pipefail +unset CDPATH + +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +REPO_ROOT=$(cd "${SCRIPT_DIR}/../.." && pwd) +WORKTREE=${CODEX_WORKTREE_PATH:-${REPO_ROOT}} +SOURCE_TREE=${CODEX_SOURCE_TREE_PATH:-${REPO_ROOT}} +NODE_HOME=${HUBBLE_NODE_HOME:-"${HOME}/.nvm/versions/node/v18.20.8"} +CODEX_HOME=${CODEX_HOME:-"${HOME}/.codex"} + +hash_text() { + printf '%s' "$1" | LC_ALL=C shasum -a 256 | awk '{print substr($1, 1, 12)}' +} + +STATE_ROOT="${CODEX_HOME}/run/toolchain-$(hash_text "${SOURCE_TREE}")" +WORKTREE_ID=$(hash_text "${WORKTREE}") +RUNTIME_DIR="${STATE_ROOT}/worktrees/${WORKTREE_ID}" +BE_RUNTIME_DIR="${STATE_ROOT}/hubble-be" + +fe_port() { + local path=${1:-${WORKTREE}} + local checksum + checksum=$(printf '%s' "${path}" | cksum | awk '{print $1}') + echo $((3001 + checksum % 99)) +} + +read_field() { + local file=$1 key=$2 + awk -F= -v key="${key}" '$1 == key {sub(/^[^=]*=/, ""); print; exit}' "${file}" +} + +process_alive() { + local pid=$1 + [[ "${pid}" =~ ^[0-9]+$ ]] && kill -0 "${pid}" 2>/dev/null +} + +process_start_time() { + ps -o lstart= -p "$1" 2>/dev/null | sed -E 's/^[[:space:]]+//;s/[[:space:]]+$//' +} + +process_is_descendant() { + local child=$1 ancestor=$2 parent + while [[ "${child}" =~ ^[0-9]+$ ]] && ((child > 1)); do + [[ "${child}" == "${ancestor}" ]] && return 0 + parent=$(ps -o ppid= -p "${child}" 2>/dev/null | tr -d ' ') + [[ -n "${parent}" && "${parent}" != "${child}" ]] || return 1 + child=${parent} + done + return 1 +} + +owner_identity_matches() { + local file=$1 pid expected_start command + pid=$(read_field "${file}" pid) + expected_start=$(read_field "${file}" start_time) + process_alive "${pid}" || return 1 + [[ -n "${expected_start}" ]] || return 1 + [[ "$(process_start_time "${pid}")" == "${expected_start}" ]] || return 1 + command=$(ps -o command= -p "${pid}" 2>/dev/null || true) + [[ "${command}" == *".codex/scripts/hubble.sh"* ]] +} + +owner_service_matches() { + local file=$1 pid port listener + owner_identity_matches "${file}" || return 1 + pid=$(read_field "${file}" pid) + port=$(read_field "${file}" port) + listener=$(lsof -tiTCP:"${port}" -sTCP:LISTEN 2>/dev/null | head -1 || true) + [[ -z "${listener}" ]] || process_is_descendant "${listener}" "${pid}" +} + +acquire_state_lock() { + local attempt probe + mkdir -p "${STATE_ROOT}" + sleep 5 & + probe=$! + STATE_LOCK_PID=$(ps -o ppid= -p "${probe}" | tr -d ' ') + kill "${probe}" 2>/dev/null || true + wait "${probe}" 2>/dev/null || true + [[ "${STATE_LOCK_PID}" =~ ^[0-9]+$ ]] || { + echo "ERROR: unable to identify owner-lock process" >&2 + return 1 + } + if command -v flock >/dev/null 2>&1; then + STATE_LOCK_KIND=flock + STATE_LOCK_PATH="${STATE_ROOT}/.owner.lock" + exec 9>"${STATE_LOCK_PATH}" + flock -w 12 9 || { + echo "ERROR: timed out waiting for Hubble owner lock" >&2 + return 1 + } + return + fi + if command -v shlock >/dev/null 2>&1; then + STATE_LOCK_KIND=shlock + STATE_LOCK_PATH="${STATE_ROOT}/.owner.lock" + for ((attempt = 0; attempt < 240; attempt++)); do + shlock -f "${STATE_LOCK_PATH}" -p "${STATE_LOCK_PID}" && return + sleep 0.05 + done + echo "ERROR: timed out waiting for Hubble owner lock" >&2 + return 1 + fi + + STATE_LOCK_KIND="mkdir" + STATE_LOCK_PATH="${STATE_ROOT}/.owner-lock" + STATE_LOCK_TOKEN="${STATE_LOCK_PID}-${RANDOM}" + for ((attempt = 0; attempt < 240; attempt++)); do + if mkdir "${STATE_LOCK_PATH}" 2>/dev/null; then + printf '%s\n' "${STATE_LOCK_TOKEN}" > "${STATE_LOCK_PATH}/owner" + return + fi + sleep 0.05 + done + echo "ERROR: owner lock is stuck; remove ${STATE_LOCK_PATH} if no action runs" >&2 + return 1 +} + +release_state_lock() { + local holder + case "${STATE_LOCK_KIND:-}" in + flock) + flock -u 9 2>/dev/null || true + exec 9>&- + ;; + shlock) + holder=$(cat "${STATE_LOCK_PATH}" 2>/dev/null || true) + [[ "${holder}" == "${STATE_LOCK_PID}" ]] && rm -f "${STATE_LOCK_PATH}" + ;; + mkdir) + holder=$(cat "${STATE_LOCK_PATH}/owner" 2>/dev/null || true) + [[ "${holder}" == "${STATE_LOCK_TOKEN}" ]] && rm -rf "${STATE_LOCK_PATH}" + ;; + esac +} + +with_state_lock() ( + acquire_state_lock + trap release_state_lock EXIT + "$@" +) + +new_token() { + printf '%s-%s-%s\n' "$$" "$(date +%s)" "${RANDOM}" +} + +write_owner() { + local file=$1 kind=$2 pid=$3 port=$4 token=$5 temp + mkdir -p "$(dirname "${file}")" + temp="${file}.tmp.${pid}.${RANDOM}" + { + echo "kind=${kind}" + echo "pid=${pid}" + echo "start_time=$(process_start_time "${pid}")" + echo "token=${token}" + echo "port=${port}" + echo "worktree=${WORKTREE}" + echo "branch=$(git -C "${WORKTREE}" branch --show-current 2>/dev/null || true)" + echo "head=$(git -C "${WORKTREE}" rev-parse --short HEAD 2>/dev/null || true)" + } > "${temp}" + mv "${temp}" "${file}" +} + +remove_owner_if_token() { + local file=$1 expected_token=$2 + [[ -f "${file}" ]] || return + if [[ "$(read_field "${file}" token)" == "${expected_token}" ]] && \ + [[ "$(read_field "${file}" pid)" == "$$" ]]; then + rm -f "${file}" + fi +} + +cleanup_claim() { + [[ -n "${SERVICE_OWNER:-}" && -n "${SERVICE_TOKEN:-}" ]] || return + [[ -f "${SERVICE_OWNER}" ]] || return + [[ "$(read_field "${SERVICE_OWNER}" token)" == "${SERVICE_TOKEN}" ]] || return + with_state_lock remove_owner_if_token "${SERVICE_OWNER}" "${SERVICE_TOKEN}" || true +} + +kill_tree() { + local pid=$1 child + while read -r child; do + [[ -n "${child}" ]] && kill_tree "${child}" + done < <(pgrep -P "${pid}" 2>/dev/null || true) + kill -TERM "${pid}" 2>/dev/null || true +} + +live_fe_count() { + local file pid count=0 + shopt -s nullglob + for file in "${STATE_ROOT}"/fe-*.owner; do + pid=$(read_field "${file}" pid) + if owner_service_matches "${file}"; then + count=$((count + 1)) + else + echo "WARN: removing stale FE owner for pid ${pid}" >&2 + rm -f "${file}" + fi + done + echo "${count}" +} + +claim_fe() { + local token=$1 preferred offset port owner pid owner_worktree listener + preferred=$(fe_port) + for ((offset = 0; offset < 99; offset++)); do + port=$((3001 + (preferred - 3001 + offset) % 99)) + owner="${STATE_ROOT}/fe-${port}.owner" + if [[ -f "${owner}" ]]; then + pid=$(read_field "${owner}" pid) + owner_worktree=$(read_field "${owner}" worktree) + if owner_service_matches "${owner}"; then + if [[ "${owner_worktree}" == "${WORKTREE}" ]]; then + echo "ERROR: FE already runs from this worktree on port ${port}" >&2 + return 1 + fi + continue + fi + echo "WARN: removing stale FE owner for pid ${pid}" >&2 + rm -f "${owner}" + fi + listener=$(lsof -tiTCP:"${port}" -sTCP:LISTEN 2>/dev/null | head -1 || true) + [[ -z "${listener}" ]] || continue + write_owner "${owner}" fe "$$" "${port}" "${token}" + echo "${port}" + return + done + echo "ERROR: no free Hubble FE port in 3001-3099" >&2 + return 1 +} + +start_fe() { + local port owner count token + mkdir -p "${RUNTIME_DIR}" + token=$(new_token) + port=$(with_state_lock claim_fe "${token}") + owner="${STATE_ROOT}/fe-${port}.owner" + SERVICE_OWNER=${owner} + SERVICE_TOKEN=${token} + trap cleanup_claim EXIT + + count=$(with_state_lock live_fe_count) + if ((count > 3)); then + echo "WARN: ${count} Hubble frontend servers are already running" >&2 + fi + [[ -x "${NODE_HOME}/bin/node" ]] || { + echo "ERROR: missing Node 18.20.8 at ${NODE_HOME}" >&2 + exit 1 + } + [[ -d "${WORKTREE}/hugegraph-hubble/hubble-fe/node_modules" ]] || { + echo "ERROR: frontend dependencies are missing; run the environment setup" >&2 + exit 1 + } + + export PATH="${NODE_HOME}/bin:${PATH}" + cd "${WORKTREE}/hugegraph-hubble/hubble-fe" + echo "[hubble] FE ${WORKTREE} -> http://127.0.0.1:${port}" + PORT="${port}" BROWSER=none yarn start +} + +java11_home() { + if [[ -n "${JAVA11_HOME:-}" ]] && [[ -x "${JAVA11_HOME}/bin/java" ]]; then + echo "${JAVA11_HOME}" + elif [[ -x /usr/libexec/java_home ]]; then + /usr/libexec/java_home -v 11 2>/dev/null + elif command -v java >/dev/null 2>&1 && \ + java -version 2>&1 | head -1 | grep -Eq 'version "11\.'; then + cd "$(dirname "$(command -v java)")/.." && pwd + else + echo "ERROR: Java 11 is required; set JAVA11_HOME" >&2 + return 1 + fi +} + +maven_runner() { + local java_home=$1 candidate + for candidate in "${HUBBLE_MVND_BIN:-}" \ + /opt/homebrew/opt/mvnd@1/bin/mvnd \ + /usr/local/opt/mvnd@1/bin/mvnd \ + "$(command -v mvnd 2>/dev/null || true)"; do + if [[ -n "${candidate}" ]] && [[ -x "${candidate}" ]] && \ + JAVA_HOME="${java_home}" "${candidate}" --version >/dev/null 2>&1; then + echo "${candidate}" + return + fi + done + echo mvn +} + +loader_home() { + local candidate + if [[ -n "${HUBBLE_LOADER_HOME:-}" ]]; then + echo "${HUBBLE_LOADER_HOME}" + return + fi + candidate=$(find "${SOURCE_TREE}/hugegraph-loader" -maxdepth 1 -type d \ + -name 'apache-hugegraph-loader-*' | sort | tail -1) + if [[ -z "${candidate}" ]]; then + echo "ERROR: no packaged Loader found; set HUBBLE_LOADER_HOME" >&2 + exit 1 + fi + echo "${candidate}" +} + +claim_be() { + local switch=$1 token=$2 owner="${STATE_ROOT}/be.owner" + local pid owner_worktree listener wait_for + if [[ -f "${owner}" ]]; then + pid=$(read_field "${owner}" pid) + owner_worktree=$(read_field "${owner}" worktree) + if owner_service_matches "${owner}"; then + if [[ "${owner_worktree}" == "${WORKTREE}" ]]; then + echo "[hubble] BE already runs from this worktree (pid ${pid})" >&2 + return 10 + fi + if [[ "${switch}" != true ]]; then + echo "ERROR: BE is owned by ${owner_worktree} (pid ${pid})" >&2 + echo "Use the 'Hubble BE Switch' action to replace it." >&2 + return 1 + fi + rm -f "${owner}" + kill_tree "${pid}" + wait_for=0 + while process_alive "${pid}" && ((wait_for < 50)); do + sleep 0.1 + wait_for=$((wait_for + 1)) + done + if process_alive "${pid}"; then + echo "ERROR: previous BE pid ${pid} did not stop" >&2 + return 1 + fi + else + echo "WARN: refusing to signal stale or mismatched BE pid ${pid}" >&2 + rm -f "${owner}" + fi + fi + + listener=$(lsof -tiTCP:8088 -sTCP:LISTEN 2>/dev/null | head -1 || true) + if [[ -n "${listener}" ]]; then + echo "ERROR: unowned process ${listener} is listening on port 8088" >&2 + return 1 + fi + write_owner "${owner}" be "$$" 8088 "${token}" +} + +start_be() { + local switch=${1:-false} + local owner="${STATE_ROOT}/be.owner" token claim_status + local java_home runner classpath loader config_arg=() + mkdir -p "${RUNTIME_DIR}" + mkdir -p "${BE_RUNTIME_DIR}/logs" "${BE_RUNTIME_DIR}/upload-files" + token=$(new_token) + if with_state_lock claim_be "${switch}" "${token}"; then + : + else + claim_status=$? + [[ "${claim_status}" -eq 10 ]] && return + return "${claim_status}" + fi + SERVICE_OWNER=${owner} + SERVICE_TOKEN=${token} + trap cleanup_claim EXIT + + java_home=$(java11_home) + runner=$(maven_runner "${java_home}") + classpath="${RUNTIME_DIR}/hubble-be.classpath" + echo "[hubble] compiling BE with Java 11 + ${runner}" + ( + cd "${WORKTREE}/hugegraph-hubble" + JAVA_HOME="${java_home}" "${runner}" -pl hubble-be -DskipTests compile \ + dependency:build-classpath -Dmdep.outputFile="${classpath}" -ntp + ) + [[ -s "${classpath}" ]] || { + echo "ERROR: Maven did not create ${classpath}" >&2 + exit 1 + } + + loader=$(loader_home) + if [[ -n "${HUBBLE_CONFIG_FILE:-}" ]]; then + config_arg=("${HUBBLE_CONFIG_FILE}") + fi + + cd "${BE_RUNTIME_DIR}" + echo "[hubble] BE owner: ${WORKTREE}" + "${java_home}/bin/java" \ + -Dfile.encoding=UTF-8 \ + -Dhubble.home.path="${BE_RUNTIME_DIR}" \ + -Dloader.home.path="${loader}" \ + -XX:ActiveProcessorCount=2 \ + -Xms64m -Xmx256m \ + -XX:MaxMetaspaceSize=128m \ + -XX:MaxDirectMemorySize=64m \ + -cp "${WORKTREE}/hugegraph-hubble/hubble-be/target/classes:$(<"${classpath}")" \ + org.apache.hugegraph.HugeGraphHubble "${config_arg[@]}" +} + +stop_current() { + local owner pid owner_worktree + shopt -s nullglob + for owner in "${STATE_ROOT}"/fe-*.owner "${STATE_ROOT}/be.owner"; do + [[ -f "${owner}" ]] || continue + pid=$(read_field "${owner}" pid) + owner_worktree=$(read_field "${owner}" worktree) + if [[ "${owner_worktree}" != "${WORKTREE}" ]]; then + echo "[hubble] leave ${owner_worktree} owner running" + continue + fi + if owner_service_matches "${owner}"; then + echo "[hubble] stopping pid ${pid} from ${WORKTREE}" + rm -f "${owner}" + kill_tree "${pid}" + else + echo "WARN: owner identity mismatch; not signaling pid ${pid}: ${owner}" >&2 + fi + done +} + +show_status() { + local file pid port path branch head rss + mkdir -p "${STATE_ROOT}" + printf '%-5s %-7s %-8s %-10s %s\n' TYPE PORT PID RSS_KB OWNER + shopt -s nullglob + for file in "${STATE_ROOT}"/fe-*.owner "${STATE_ROOT}/be.owner"; do + [[ -f "${file}" ]] || continue + pid=$(read_field "${file}" pid) + if ! owner_service_matches "${file}"; then + echo "WARN: stale or mismatched owner: ${file}" >&2 + rm -f "${file}" + continue + fi + port=$(read_field "${file}" port) + path=$(read_field "${file}" worktree) + branch=$(read_field "${file}" branch) + head=$(read_field "${file}" head) + rss=$(ps -o rss= -p "${pid}" | tr -d ' ') + printf '%-5s %-7s %-8s %-10s %s [%s@%s]\n' \ + "$(read_field "${file}" kind)" "${port}" "${pid}" "${rss:-?}" \ + "${path}" "${branch:-detached}" "${head}" + done +} + +case "${1:-help}" in + fe-port) fe_port "${2:-${WORKTREE}}" ;; + be-runtime) echo "${BE_RUNTIME_DIR}" ;; + _lock-hold) with_state_lock sleep "${2:-1}" ;; + fe-start) start_fe ;; + be-start) start_be false ;; + be-switch) start_be true ;; + status) with_state_lock show_status ;; + stop | cleanup) with_state_lock stop_current ;; + *) + echo "Usage: $0 {fe-port|be-runtime|fe-start|be-start|be-switch|status|stop|cleanup}" >&2 + exit 2 + ;; +esac diff --git a/.codex/scripts/infra.sh b/.codex/scripts/infra.sh new file mode 100755 index 000000000..67e9e4a27 --- /dev/null +++ b/.codex/scripts/infra.sh @@ -0,0 +1,174 @@ +#!/usr/bin/env bash + +set -euo pipefail +unset CDPATH + +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +CODEX_DIR=$(cd "${SCRIPT_DIR}/.." && pwd) +REPO_ROOT=$(cd "${CODEX_DIR}/.." && pwd) +SOURCE_TREE=${CODEX_SOURCE_TREE_PATH:-${REPO_ROOT}} + +resolve_server_repo() { + if [[ -n "${HUGEGRAPH_SERVER_REPO:-}" ]]; then + echo "${HUGEGRAPH_SERVER_REPO}" + return + fi + local sibling + sibling=$(cd "${SOURCE_TREE}/.." 2>/dev/null && pwd)/server + if [[ -d "${sibling}/docker" ]]; then + echo "${sibling}" + return + fi + echo "ERROR: set HUGEGRAPH_SERVER_REPO or place server beside toolchain" >&2 + exit 1 +} + +compose_files() { + local profile=${1:-low} + local server_repo=$2 + COMPOSE_ARGS=( + -f "${server_repo}/docker/docker-compose.yml" + -f "${CODEX_DIR}/infra/compose.low-memory.yml" + ) + if [[ "${profile}" == "balanced" ]]; then + COMPOSE_ARGS+=(-f "${CODEX_DIR}/infra/compose.balanced.yml") + elif [[ "${profile}" != "low" ]]; then + echo "ERROR: unknown profile ${profile}" >&2 + exit 2 + fi +} + +print_compose_command() { + local profile=${1:-low} server_repo arg + server_repo=$(resolve_server_repo) + [[ -f "${server_repo}/docker/docker-compose.yml" ]] || { + echo "ERROR: missing server Compose file" >&2 + exit 1 + } + compose_files "${profile}" "${server_repo}" + printf 'TOOLCHAIN_CODEX_DIR=%q HUGEGRAPH_VERSION=latest docker compose' "${CODEX_DIR}" + for arg in "${COMPOSE_ARGS[@]}"; do printf ' %q' "${arg}"; done + printf ' up -d --pull never --wait\n' +} + +warn_dirty_server() { + local server_repo=$1 dirty + dirty=$(git -C "${server_repo}" status --short 2>/dev/null || true) + if [[ -n "${dirty}" ]]; then + echo "WARN: server repo has local changes; latest images do not include them:" >&2 + echo "${dirty}" | head -20 >&2 + fi +} + +assert_docker() { + docker info >/dev/null 2>&1 || { + echo "ERROR: Docker is not running" >&2 + exit 1 + } +} + +assert_port_available_for_compose() { + local port=$1 expected_container=$2 listener container + local project='' service='' mapping='' + listener=$(lsof -tiTCP:"${port}" -sTCP:LISTEN 2>/dev/null | head -1 || true) + [[ -z "${listener}" ]] && return + container=$(docker ps --filter "name=^/${expected_container}$" \ + --format '{{.ID}}' | head -1) + if [[ -n "${container}" ]]; then + project=$(docker inspect --format \ + '{{index .Config.Labels "com.docker.compose.project"}}' "${container}") + service=$(docker inspect --format \ + '{{index .Config.Labels "com.docker.compose.service"}}' "${container}") + mapping=$(docker port "${container}" "${port}/tcp" 2>/dev/null || true) + fi + if [[ -z "${container}" || "${project}" != hugegraph-single || \ + "${service}" != "${expected_container#hg-}" || \ + "${mapping}" != *":${port}"* ]]; then + echo "ERROR: port ${port} is owned by non-Compose pid ${listener}" >&2 + exit 1 + fi +} + +hstore_start() { + local profile=${1:-low} server_repo + server_repo=$(resolve_server_repo) + assert_docker + warn_dirty_server "${server_repo}" + assert_port_available_for_compose 8080 hg-server + assert_port_available_for_compose 8520 hg-store + assert_port_available_for_compose 8620 hg-pd + compose_files "${profile}" "${server_repo}" + echo "[infra] starting HStore 1+1+1 (${profile}, local latest images)" + TOOLCHAIN_CODEX_DIR="${CODEX_DIR}" HUGEGRAPH_VERSION=latest \ + docker compose "${COMPOSE_ARGS[@]}" up -d --pull never --wait +} + +infra_status() { + local server_repo + server_repo=$(resolve_server_repo) + assert_docker + compose_files low "${server_repo}" + TOOLCHAIN_CODEX_DIR="${CODEX_DIR}" HUGEGRAPH_VERSION=latest \ + docker compose "${COMPOSE_ARGS[@]}" ps + echo + docker stats --no-stream --format \ + 'table {{.Name}}\t{{.MemUsage}}\t{{.CPUPerc}}' \ + hg-pd hg-store hg-server 2>/dev/null || true + echo + lsof -nP -iTCP:8080 -sTCP:LISTEN 2>/dev/null || true +} + +infra_stop() { + local server_repo + server_repo=$(resolve_server_repo) + assert_docker + compose_files low "${server_repo}" + echo "[infra] stopping Compose services; named volumes are preserved" + TOOLCHAIN_CODEX_DIR="${CODEX_DIR}" HUGEGRAPH_VERSION=latest \ + docker compose "${COMPOSE_ARGS[@]}" down +} + +infra_reset() { + local server_repo confirmation=${CONFIRM_INFRA_RESET:-} + server_repo=$(resolve_server_repo) + assert_docker + compose_files low "${server_repo}" + if [[ "${confirmation}" != RESET ]]; then + if [[ ! -t 0 ]]; then + echo "ERROR: destructive reset requires CONFIRM_INFRA_RESET=RESET" >&2 + exit 1 + fi + read -r -p "Type RESET to delete local HStore Compose volumes: " confirmation + fi + [[ "${confirmation}" == RESET ]] || { + echo "[infra] reset cancelled" + return + } + echo "[infra] stopping Compose services and deleting their named volumes" + TOOLCHAIN_CODEX_DIR="${CODEX_DIR}" HUGEGRAPH_VERSION=latest \ + docker compose "${COMPOSE_ARGS[@]}" down --volumes --remove-orphans +} + +infra_pull() { + local server_repo + server_repo=$(resolve_server_repo) + assert_docker + compose_files low "${server_repo}" + echo "[infra] explicitly updating latest images" + TOOLCHAIN_CODEX_DIR="${CODEX_DIR}" HUGEGRAPH_VERSION=latest \ + docker compose "${COMPOSE_ARGS[@]}" pull +} + +case "${1:-help}" in + resolve-server-repo) resolve_server_repo ;; + compose-command) print_compose_command "${2:-low}" ;; + hstore-start) hstore_start "${2:-low}" ;; + status) infra_status ;; + stop) infra_stop ;; + reset) infra_reset ;; + pull) infra_pull ;; + *) + echo "Usage: $0 {resolve-server-repo|compose-command|hstore-start|status|stop|reset|pull}" >&2 + exit 2 + ;; +esac diff --git a/.codex/scripts/setup.sh b/.codex/scripts/setup.sh new file mode 100755 index 000000000..580079cfa --- /dev/null +++ b/.codex/scripts/setup.sh @@ -0,0 +1,92 @@ +#!/usr/bin/env bash + +set -euo pipefail +unset CDPATH + +WORKTREE=${CODEX_WORKTREE_PATH:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)} +NODE_HOME=${HUBBLE_NODE_HOME:-"${HOME}/.nvm/versions/node/v18.20.8"} +FE_DIR="${WORKTREE}/hugegraph-hubble/hubble-fe" +DRY_RUN=false + +if [[ "${1:-}" == "--dry-run" ]]; then + DRY_RUN=true +fi + +echo "[setup] worktree: ${WORKTREE}" +echo "[setup] require Node 18.20.8 from ${NODE_HOME}" + +if ${DRY_RUN}; then + echo "[setup] yarn install --frozen-lockfile --prefer-offline --non-interactive" + exit 0 +fi + +if [[ ! -x "${NODE_HOME}/bin/node" ]]; then + echo "ERROR: Node 18.20.8 is not installed at ${NODE_HOME}" >&2 + exit 1 +fi + +export PATH="${NODE_HOME}/bin:${PATH}" +[[ "$(node --version)" == "v18.20.8" ]] || { + echo "ERROR: expected Node v18.20.8, got $(node --version)" >&2 + exit 1 +} +command -v yarn >/dev/null || { + echo "ERROR: yarn is not available with Node 18.20.8" >&2 + exit 1 +} + +if [[ ! -f "${FE_DIR}/yarn.lock" ]]; then + echo "ERROR: missing ${FE_DIR}/yarn.lock" >&2 + exit 1 +fi + +lock_hash=$(LC_ALL=C shasum -a 256 "${FE_DIR}/yarn.lock" | awk '{print $1}') +stamp="${FE_DIR}/node_modules/.codex-yarn-lock.sha256" +if [[ -f "${stamp}" ]] && [[ "$(<"${stamp}")" == "${lock_hash}" ]]; then + echo "[setup] frontend dependencies already match yarn.lock" +else + echo "[setup] installing frontend dependencies from the shared Yarn cache" + ( + cd "${FE_DIR}" + yarn install --frozen-lockfile --prefer-offline --non-interactive + ) + printf '%s\n' "${lock_hash}" > "${stamp}" +fi + +if [[ -n "${JAVA11_HOME:-}" ]] && [[ -x "${JAVA11_HOME}/bin/java" ]]; then + java_home=${JAVA11_HOME} +elif [[ -x /usr/libexec/java_home ]] && \ + java_home=$(/usr/libexec/java_home -v 11 2>/dev/null); then + : +elif command -v java >/dev/null 2>&1 && \ + java -version 2>&1 | head -1 | grep -Eq 'version "11\.'; then + java_home=$(cd "$(dirname "$(command -v java)")/.." && pwd) +else + java_home= +fi + +if [[ -n "${java_home}" ]]; then + echo "[setup] Java 11: ${java_home}" +else + echo "ERROR: Java 11 is required for Hubble BE development" >&2 + exit 1 +fi + +mvnd_bin= +for candidate in "${HUBBLE_MVND_BIN:-}" \ + /opt/homebrew/opt/mvnd@1/bin/mvnd \ + /usr/local/opt/mvnd@1/bin/mvnd \ + "$(command -v mvnd 2>/dev/null || true)"; do + if [[ -n "${candidate}" ]] && [[ -x "${candidate}" ]] && \ + JAVA_HOME="${java_home}" "${candidate}" --version >/dev/null 2>&1; then + mvnd_bin=${candidate} + break + fi +done +if [[ -n "${mvnd_bin}" ]]; then + echo "[setup] Java 11 + mvnd: ${mvnd_bin}" +else + echo "[setup] Java 11 + mvnd unavailable; Hubble BE will use Java 11 + Maven" +fi + +echo "[setup] complete" diff --git a/.codex/scripts/verify.sh b/.codex/scripts/verify.sh new file mode 100755 index 000000000..c2189db8c --- /dev/null +++ b/.codex/scripts/verify.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash + +set -euo pipefail +unset CDPATH + +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +REPO_ROOT=$(cd "${SCRIPT_DIR}/../.." && pwd) +WORKTREE=${CODEX_WORKTREE_PATH:-${REPO_ROOT}} +NODE_HOME=${HUBBLE_NODE_HOME:-"${HOME}/.nvm/versions/node/v18.20.8"} +if [[ -n "${JAVA11_HOME:-}" ]] && [[ -x "${JAVA11_HOME}/bin/java" ]]; then + JAVA_HOME=${JAVA11_HOME} +elif [[ -x /usr/libexec/java_home ]]; then + JAVA_HOME=$(/usr/libexec/java_home -v 11) +elif command -v java >/dev/null 2>&1 && \ + java -version 2>&1 | head -1 | grep -Eq 'version "11\.'; then + JAVA_HOME=$(cd "$(dirname "$(command -v java)")/.." && pwd) +else + echo "ERROR: Java 11 is required; set JAVA11_HOME" >&2 + exit 1 +fi +export JAVA_HOME PATH="${NODE_HOME}/bin:${PATH}" + +MAVEN= +for candidate in "${HUBBLE_MVND_BIN:-}" \ + /opt/homebrew/opt/mvnd@1/bin/mvnd \ + /usr/local/opt/mvnd@1/bin/mvnd \ + "$(command -v mvnd 2>/dev/null || true)"; do + if [[ -n "${candidate}" ]] && [[ -x "${candidate}" ]] && \ + "${candidate}" --version >/dev/null 2>&1; then + MAVEN=${candidate} + break + fi +done +MAVEN=${MAVEN:-mvn} + +fast_verify() { + ( + cd "${WORKTREE}/hugegraph-hubble/hubble-fe" + yarn lint + yarn i18n:check + ) + ( + cd "${WORKTREE}" + "${MAVEN}" -pl hugegraph-hubble/hubble-be -am \ + -DskipTests compile -Djacoco.skip=true -ntp + ) +} + +full_verify() { + fast_verify + ( + cd "${WORKTREE}/hugegraph-hubble/hubble-fe" + CI=true yarn test --watchAll=false --runInBand + CI=true yarn build + ) + ( + cd "${WORKTREE}" + "${MAVEN}" test -P unit-test -pl hugegraph-hubble/hubble-be \ + -Djacoco.skip=true -ntp + ) +} + +case "${1:-fast}" in + fast) fast_verify ;; + full) full_verify ;; + *) echo "Usage: $0 {fast|full}" >&2; exit 2 ;; +esac diff --git a/.codex/tests/environment_test.sh b/.codex/tests/environment_test.sh new file mode 100755 index 000000000..abfaab729 --- /dev/null +++ b/.codex/tests/environment_test.sh @@ -0,0 +1,200 @@ +#!/usr/bin/env bash + +set -euo pipefail +unset CDPATH + +CODEX_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +REPO_ROOT=$(cd "${CODEX_DIR}/.." && pwd) +TEST_TMP=$(mktemp -d "${TMPDIR:-/tmp}/toolchain-env-test.XXXXXX") +TEST_TMP=$(cd "${TEST_TMP}" && pwd) +trap 'rm -rf "${TEST_TMP}"' EXIT + +fail() { + echo "FAIL: $*" >&2 + exit 1 +} + +assert_contains() { + local actual=$1 + local expected=$2 + [[ "${actual}" == *"${expected}"* ]] || + fail "expected '${actual}' to contain '${expected}'" +} + +test_environment_toml() { + python3 - "${CODEX_DIR}/environments/environment.toml" <<'PY' +import pathlib +import sys +import tomllib + +path = pathlib.Path(sys.argv[1]) +with path.open("rb") as stream: + config = tomllib.load(stream) + +assert config["version"] == 1 +assert config["name"] == "toolchain-hubble" +assert config["setup"]["script"] +assert config["cleanup"]["script"] +names = {action["name"] for action in config["actions"]} +required = { + "Hubble FE Dev", + "Hubble BE Dev", + "Hubble Status", + "Hubble Stop", + "HStore Low Memory", + "Infrastructure Status", + "Infrastructure Stop", + "Infrastructure Reset", +} +assert required <= names, required - names +PY +} + +test_setup_dry_run() { + local output + output=$(CODEX_WORKTREE_PATH="${REPO_ROOT}" \ + "${CODEX_DIR}/scripts/setup.sh" --dry-run) + assert_contains "${output}" "Node 18.20.8" + assert_contains "${output}" "yarn install --frozen-lockfile" +} + +test_stable_frontend_ports() { + local first second other + first=$("${CODEX_DIR}/scripts/hubble.sh" fe-port "/tmp/worktree-a") + second=$("${CODEX_DIR}/scripts/hubble.sh" fe-port "/tmp/worktree-a") + other=$("${CODEX_DIR}/scripts/hubble.sh" fe-port "/tmp/worktree-b") + + [[ "${first}" == "${second}" ]] || fail "port assignment is not stable" + ((first >= 3001 && first <= 3099)) || fail "port ${first} is outside 3001-3099" + [[ "${first}" != "${other}" ]] || fail "fixture paths unexpectedly collide" +} + +test_concurrent_frontend_claim() { + local worktree="${TEST_TMP}/fe-worktree" + local fake_node="${TEST_TMP}/fake-node" + local first_output="${TEST_TMP}/first-fe.log" + local second_output="${TEST_TMP}/second-fe.log" + local first_pid + mkdir -p "${worktree}/hugegraph-hubble/hubble-fe/node_modules" \ + "${fake_node}/bin" + printf '#!/bin/sh\nexit 0\n' > "${fake_node}/bin/node" + printf '#!/bin/sh\nsleep 1\n' > "${fake_node}/bin/yarn" + chmod +x "${fake_node}/bin/node" "${fake_node}/bin/yarn" + + CODEX_HOME="${TEST_TMP}/codex-home" \ + CODEX_SOURCE_TREE_PATH="${TEST_TMP}/source" \ + CODEX_WORKTREE_PATH="${worktree}" \ + HUBBLE_NODE_HOME="${fake_node}" \ + "${CODEX_DIR}/scripts/hubble.sh" fe-start >"${first_output}" 2>&1 & + first_pid=$! + sleep 0.2 + if CODEX_HOME="${TEST_TMP}/codex-home" \ + CODEX_SOURCE_TREE_PATH="${TEST_TMP}/source" \ + CODEX_WORKTREE_PATH="${worktree}" \ + HUBBLE_NODE_HOME="${fake_node}" \ + "${CODEX_DIR}/scripts/hubble.sh" fe-start >"${second_output}" 2>&1; then + fail "concurrent FE start unexpectedly acquired a second owner" + fi + wait "${first_pid}" + grep -q 'FE already runs from this worktree' "${second_output}" + [[ -z "$(find "${TEST_TMP}/codex-home" -name 'fe-*.owner' -print -quit)" ]] || + fail "FE owner was not removed by its token-aware EXIT cleanup" +} + +test_shared_backend_runtime() { + local runtime + runtime=$(CODEX_HOME="${TEST_TMP}/codex-home" \ + CODEX_SOURCE_TREE_PATH="${TEST_TMP}/source" \ + CODEX_WORKTREE_PATH="${TEST_TMP}/worktree-a" \ + "${CODEX_DIR}/scripts/hubble.sh" be-runtime) + assert_contains "${runtime}" "/hubble-be" + [[ "${runtime}" != *"worktree-a"* ]] || + fail "BE runtime is still scoped to a source worktree" +} + +test_owner_lock_contention() { + local lock_home="${TEST_TMP}/lock-home" index=0 + while ((index < 20)); do + CODEX_HOME="${lock_home}" \ + "${CODEX_DIR}/scripts/hubble.sh" status >/dev/null & + index=$((index + 1)) + done + wait + [[ -z "$(find "${lock_home}" -name .owner-lock -print -quit)" ]] || + fail "owner lock leaked after concurrent status calls" +} + +test_owner_lock_excludes_long_critical_section() { + local lock_home="${TEST_TMP}/long-lock-home" first_pid second_pid + CODEX_HOME="${lock_home}" \ + "${CODEX_DIR}/scripts/hubble.sh" _lock-hold 1 & + first_pid=$! + sleep 0.1 + CODEX_HOME="${lock_home}" \ + "${CODEX_DIR}/scripts/hubble.sh" _lock-hold 0 & + second_pid=$! + sleep 0.2 + kill -0 "${second_pid}" 2>/dev/null || + fail "second action entered a long owner critical section" + wait "${first_pid}" + wait "${second_pid}" +} + +test_server_repo_resolution() { + local output + output=$(HUGEGRAPH_SERVER_REPO="${TEST_TMP}/server" \ + "${CODEX_DIR}/scripts/infra.sh" resolve-server-repo) + [[ "${output}" == "${TEST_TMP}/server" ]] || fail "explicit server repo was ignored" + + mkdir -p "${TEST_TMP}/graph/toolchain" "${TEST_TMP}/graph/server/docker" + output=$(CODEX_SOURCE_TREE_PATH="${TEST_TMP}/graph/toolchain" \ + "${CODEX_DIR}/scripts/infra.sh" resolve-server-repo) + [[ "${output}" == "${TEST_TMP}/graph/server" ]] || fail "sibling server repo was not found" +} + +test_compose_command_is_offline_and_layered() { + local server_repo="${TEST_TMP}/server" + local output + mkdir -p "${server_repo}/docker" + : > "${server_repo}/docker/docker-compose.yml" + + output=$(HUGEGRAPH_SERVER_REPO="${server_repo}" \ + "${CODEX_DIR}/scripts/infra.sh" compose-command low) + assert_contains "${output}" "docker-compose.yml" + assert_contains "${output}" "compose.low-memory.yml" + assert_contains "${output}" "--pull never" +} + +test_server_config_patch() { + local conf="${TEST_TMP}/server-conf" + mkdir -p "${conf}" + cat > "${conf}/rest-server.properties" <<'EOF' +batch.max_write_threads=16 +restserver.min_free_memory=512 +EOF + cat > "${conf}/gremlin-server.yaml" <<'EOF' +threadPoolWorker: 8 +gremlinPool: 8 +EOF + + HG_SERVER_CONF_DIR="${conf}" \ + "${CODEX_DIR}/infra/patch-server-config.sh" --patch-only + + grep -qx 'batch.max_write_threads=2' "${conf}/rest-server.properties" + grep -qx 'restserver.min_free_memory=16' "${conf}/rest-server.properties" + grep -qx 'threadPoolWorker: 2' "${conf}/gremlin-server.yaml" + grep -qx 'gremlinPool: 2' "${conf}/gremlin-server.yaml" +} + +test_environment_toml +test_setup_dry_run +test_stable_frontend_ports +test_concurrent_frontend_claim +test_shared_backend_runtime +test_owner_lock_contention +test_owner_lock_excludes_long_critical_section +test_server_repo_resolution +test_compose_command_is_offline_and_layered +test_server_config_patch + +echo "PASS: toolchain local environment tests" diff --git a/hugegraph-hubble/README.md b/hugegraph-hubble/README.md index cbc13ad04..7f4294e80 100644 --- a/hugegraph-hubble/README.md +++ b/hugegraph-hubble/README.md @@ -37,6 +37,13 @@ shortcut. Build the release package with `mvnd package -DskipTests`; these development commands do not change packaged runtime behavior. +### Codex worktree workflow (optional) + +For Codex users, this repository provides a managed local Hubble environment +with stable frontend ports, shared backend ownership, optional local HStore +profiles, and verification actions. See the +[Codex environment guide](../.codex/README.md). + Native Store metrics use an operator-managed exact-origin allowlist in addition to PD topology and metrics-target discovery. The packaged default `operations.store.allowed_targets=[http://127.0.0.1:8520,http://[::1]:8520]` From 5214f5b9a320499bcef84d879081d12208639a3c Mon Sep 17 00:00:00 2001 From: imbajin Date: Wed, 22 Jul 2026 00:01:17 +0800 Subject: [PATCH 2/2] fix: make Codex HStore profile ready for local testing --- .codex/README.md | 11 +++ .codex/environments/environment.toml | 17 ++++ .codex/infra/compose.balanced.yml | 17 ++++ .codex/infra/compose.low-memory.yml | 18 ++++ .codex/infra/patch-server-config.sh | 26 +++++- .codex/scripts/hubble.sh | 18 +++- .codex/scripts/infra.sh | 17 ++++ .codex/scripts/setup.sh | 17 ++++ .codex/scripts/verify.sh | 17 ++++ .codex/tests/environment_test.sh | 131 ++++++++++++--------------- 10 files changed, 211 insertions(+), 78 deletions(-) diff --git a/.codex/README.md b/.codex/README.md index 601de5538..4f75c3710 100644 --- a/.codex/README.md +++ b/.codex/README.md @@ -13,6 +13,17 @@ frontend dependencies without building or starting services. - HStore uses the sibling server repository's Compose file plus the tracked low-memory override. Images use the local `latest` tag and never pull during startup. `Infrastructure Pull` is the explicit update operation. +- Low-memory Server tuning is best-effort across `latest` image layouts: known + properties are applied even when absent from the template, while unavailable + YAML settings produce a warning and keep the image default. It is a local + functional-test profile, not a memory or compatibility guarantee. +- HStore starts with Server access control enabled. Connect Hubble to + `http://127.0.0.1:8080` as `admin` with password `pa`, or set + `HUBBLE_SERVER_PASSWORD` before the first start to use a local alternative. + The password only initializes a new local data volume; stopping services + preserves the original password. Use `Infrastructure Reset` to initialize + fresh local data if that password is lost. This known credential is only for + local testing; never reuse it elsewhere. - `Infrastructure Stop` preserves HStore data. `Infrastructure Reset` requires typing `RESET` before it deletes the Compose volumes. - Runtime ownership files live under `~/.codex/run/`, not in a worktree. diff --git a/.codex/environments/environment.toml b/.codex/environments/environment.toml index 460df2ff0..63486d8cd 100644 --- a/.codex/environments/environment.toml +++ b/.codex/environments/environment.toml @@ -1,3 +1,20 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + version = 1 name = "toolchain-hubble" diff --git a/.codex/infra/compose.balanced.yml b/.codex/infra/compose.balanced.yml index 5ead46160..f71fc3a31 100644 --- a/.codex/infra/compose.balanced.yml +++ b/.codex/infra/compose.balanced.yml @@ -1,3 +1,20 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + services: pd: mem_limit: 640m diff --git a/.codex/infra/compose.low-memory.yml b/.codex/infra/compose.low-memory.yml index 2e6fce1fd..d8f544c8c 100644 --- a/.codex/infra/compose.low-memory.yml +++ b/.codex/infra/compose.low-memory.yml @@ -1,3 +1,20 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + services: pd: pull_policy: never @@ -72,6 +89,7 @@ services: - "--" - /opt/toolchain-low-memory/patch-server-config.sh environment: + PASSWORD: ${HUBBLE_SERVER_PASSWORD:-pa} JAVA_OPTS: >- -Xms128m -Xmx192m -XX:ActiveProcessorCount=4 diff --git a/.codex/infra/patch-server-config.sh b/.codex/infra/patch-server-config.sh index 90514e6fb..1f17832c6 100755 --- a/.codex/infra/patch-server-config.sh +++ b/.codex/infra/patch-server-config.sh @@ -1,5 +1,22 @@ #!/usr/bin/env bash +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + set -euo pipefail unset CDPATH @@ -15,16 +32,17 @@ set_property() { "s|^[[:space:]]*${escaped}[[:space:]]*=.*|${key}=${value}|" "${file}" rm -f "${file}.bak" else + echo "WARN: property '${key}' is not declared in ${file}; adding low-memory override" >&2 printf '%s=%s\n' "${key}" "${value}" >> "${file}" fi } set_yaml_scalar() { local file=$1 key=$2 value=$3 - grep -qE "^[[:space:]]*${key}:" "${file}" || { - echo "ERROR: missing YAML key '${key}' in ${file}" >&2 - exit 1 - } + if ! grep -qE "^[[:space:]]*${key}:" "${file}"; then + echo "WARN: optional YAML key '${key}' is unavailable in ${file}; keeping image default" >&2 + return + fi sed -E -i.bak \ "s|^([[:space:]]*)${key}:.*|\\1${key}: ${value}|" "${file}" rm -f "${file}.bak" diff --git a/.codex/scripts/hubble.sh b/.codex/scripts/hubble.sh index 0ee432f00..96560dc40 100755 --- a/.codex/scripts/hubble.sh +++ b/.codex/scripts/hubble.sh @@ -1,5 +1,22 @@ #!/usr/bin/env bash +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + set -euo pipefail unset CDPATH @@ -444,7 +461,6 @@ show_status() { case "${1:-help}" in fe-port) fe_port "${2:-${WORKTREE}}" ;; be-runtime) echo "${BE_RUNTIME_DIR}" ;; - _lock-hold) with_state_lock sleep "${2:-1}" ;; fe-start) start_fe ;; be-start) start_be false ;; be-switch) start_be true ;; diff --git a/.codex/scripts/infra.sh b/.codex/scripts/infra.sh index 67e9e4a27..703b12c92 100755 --- a/.codex/scripts/infra.sh +++ b/.codex/scripts/infra.sh @@ -1,5 +1,22 @@ #!/usr/bin/env bash +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + set -euo pipefail unset CDPATH diff --git a/.codex/scripts/setup.sh b/.codex/scripts/setup.sh index 580079cfa..58a731c4d 100755 --- a/.codex/scripts/setup.sh +++ b/.codex/scripts/setup.sh @@ -1,5 +1,22 @@ #!/usr/bin/env bash +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + set -euo pipefail unset CDPATH diff --git a/.codex/scripts/verify.sh b/.codex/scripts/verify.sh index c2189db8c..e1cb6a196 100755 --- a/.codex/scripts/verify.sh +++ b/.codex/scripts/verify.sh @@ -1,5 +1,22 @@ #!/usr/bin/env bash +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + set -euo pipefail unset CDPATH diff --git a/.codex/tests/environment_test.sh b/.codex/tests/environment_test.sh index abfaab729..a868120a5 100755 --- a/.codex/tests/environment_test.sh +++ b/.codex/tests/environment_test.sh @@ -1,5 +1,22 @@ #!/usr/bin/env bash +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + set -euo pipefail unset CDPATH @@ -59,46 +76,35 @@ test_setup_dry_run() { } test_stable_frontend_ports() { - local first second other + local first second first=$("${CODEX_DIR}/scripts/hubble.sh" fe-port "/tmp/worktree-a") second=$("${CODEX_DIR}/scripts/hubble.sh" fe-port "/tmp/worktree-a") - other=$("${CODEX_DIR}/scripts/hubble.sh" fe-port "/tmp/worktree-b") [[ "${first}" == "${second}" ]] || fail "port assignment is not stable" ((first >= 3001 && first <= 3099)) || fail "port ${first} is outside 3001-3099" - [[ "${first}" != "${other}" ]] || fail "fixture paths unexpectedly collide" } test_concurrent_frontend_claim() { - local worktree="${TEST_TMP}/fe-worktree" - local fake_node="${TEST_TMP}/fake-node" - local first_output="${TEST_TMP}/first-fe.log" - local second_output="${TEST_TMP}/second-fe.log" - local first_pid - mkdir -p "${worktree}/hugegraph-hubble/hubble-fe/node_modules" \ - "${fake_node}/bin" + local worktree="${TEST_TMP}/fe-worktree" fake_node="${TEST_TMP}/fake-node" + local first_log="${TEST_TMP}/first-fe.log" first_pid + mkdir -p "${worktree}/hugegraph-hubble/hubble-fe/node_modules" "${fake_node}/bin" printf '#!/bin/sh\nexit 0\n' > "${fake_node}/bin/node" printf '#!/bin/sh\nsleep 1\n' > "${fake_node}/bin/yarn" chmod +x "${fake_node}/bin/node" "${fake_node}/bin/yarn" - CODEX_HOME="${TEST_TMP}/codex-home" \ - CODEX_SOURCE_TREE_PATH="${TEST_TMP}/source" \ - CODEX_WORKTREE_PATH="${worktree}" \ - HUBBLE_NODE_HOME="${fake_node}" \ - "${CODEX_DIR}/scripts/hubble.sh" fe-start >"${first_output}" 2>&1 & + CODEX_HOME="${TEST_TMP}/codex-home" CODEX_SOURCE_TREE_PATH="${TEST_TMP}/source" \ + CODEX_WORKTREE_PATH="${worktree}" HUBBLE_NODE_HOME="${fake_node}" \ + "${CODEX_DIR}/scripts/hubble.sh" fe-start >"${first_log}" 2>&1 & first_pid=$! sleep 0.2 - if CODEX_HOME="${TEST_TMP}/codex-home" \ - CODEX_SOURCE_TREE_PATH="${TEST_TMP}/source" \ - CODEX_WORKTREE_PATH="${worktree}" \ - HUBBLE_NODE_HOME="${fake_node}" \ - "${CODEX_DIR}/scripts/hubble.sh" fe-start >"${second_output}" 2>&1; then - fail "concurrent FE start unexpectedly acquired a second owner" + if CODEX_HOME="${TEST_TMP}/codex-home" CODEX_SOURCE_TREE_PATH="${TEST_TMP}/source" \ + CODEX_WORKTREE_PATH="${worktree}" HUBBLE_NODE_HOME="${fake_node}" \ + "${CODEX_DIR}/scripts/hubble.sh" fe-start >/dev/null 2>&1; then + fail "duplicate frontend start unexpectedly succeeded" fi wait "${first_pid}" - grep -q 'FE already runs from this worktree' "${second_output}" [[ -z "$(find "${TEST_TMP}/codex-home" -name 'fe-*.owner' -print -quit)" ]] || - fail "FE owner was not removed by its token-aware EXIT cleanup" + fail "frontend owner was not cleaned up" } test_shared_backend_runtime() { @@ -112,46 +118,6 @@ test_shared_backend_runtime() { fail "BE runtime is still scoped to a source worktree" } -test_owner_lock_contention() { - local lock_home="${TEST_TMP}/lock-home" index=0 - while ((index < 20)); do - CODEX_HOME="${lock_home}" \ - "${CODEX_DIR}/scripts/hubble.sh" status >/dev/null & - index=$((index + 1)) - done - wait - [[ -z "$(find "${lock_home}" -name .owner-lock -print -quit)" ]] || - fail "owner lock leaked after concurrent status calls" -} - -test_owner_lock_excludes_long_critical_section() { - local lock_home="${TEST_TMP}/long-lock-home" first_pid second_pid - CODEX_HOME="${lock_home}" \ - "${CODEX_DIR}/scripts/hubble.sh" _lock-hold 1 & - first_pid=$! - sleep 0.1 - CODEX_HOME="${lock_home}" \ - "${CODEX_DIR}/scripts/hubble.sh" _lock-hold 0 & - second_pid=$! - sleep 0.2 - kill -0 "${second_pid}" 2>/dev/null || - fail "second action entered a long owner critical section" - wait "${first_pid}" - wait "${second_pid}" -} - -test_server_repo_resolution() { - local output - output=$(HUGEGRAPH_SERVER_REPO="${TEST_TMP}/server" \ - "${CODEX_DIR}/scripts/infra.sh" resolve-server-repo) - [[ "${output}" == "${TEST_TMP}/server" ]] || fail "explicit server repo was ignored" - - mkdir -p "${TEST_TMP}/graph/toolchain" "${TEST_TMP}/graph/server/docker" - output=$(CODEX_SOURCE_TREE_PATH="${TEST_TMP}/graph/toolchain" \ - "${CODEX_DIR}/scripts/infra.sh" resolve-server-repo) - [[ "${output}" == "${TEST_TMP}/graph/server" ]] || fail "sibling server repo was not found" -} - test_compose_command_is_offline_and_layered() { local server_repo="${TEST_TMP}/server" local output @@ -167,23 +133,44 @@ test_compose_command_is_offline_and_layered() { test_server_config_patch() { local conf="${TEST_TMP}/server-conf" + local output mkdir -p "${conf}" cat > "${conf}/rest-server.properties" <<'EOF' batch.max_write_threads=16 -restserver.min_free_memory=512 EOF cat > "${conf}/gremlin-server.yaml" <<'EOF' -threadPoolWorker: 8 -gremlinPool: 8 +other: 1 EOF - HG_SERVER_CONF_DIR="${conf}" \ - "${CODEX_DIR}/infra/patch-server-config.sh" --patch-only + output=$(HG_SERVER_CONF_DIR="${conf}" \ + "${CODEX_DIR}/infra/patch-server-config.sh" --patch-only 2>&1) grep -qx 'batch.max_write_threads=2' "${conf}/rest-server.properties" grep -qx 'restserver.min_free_memory=16' "${conf}/rest-server.properties" - grep -qx 'threadPoolWorker: 2' "${conf}/gremlin-server.yaml" - grep -qx 'gremlinPool: 2' "${conf}/gremlin-server.yaml" + grep -qx 'other: 1' "${conf}/gremlin-server.yaml" + assert_contains "${output}" "WARN: property 'restserver.min_free_memory'" + assert_contains "${output}" "WARN: optional YAML key 'threadPoolWorker'" + assert_contains "${output}" "WARN: optional YAML key 'gremlinPool'" +} + +test_server_config_patch_requires_config_files() { + local missing_rest="${TEST_TMP}/server-conf-missing-rest" + local missing_yaml="${TEST_TMP}/server-conf-missing-yaml" output + mkdir -p "${missing_rest}" "${missing_yaml}" + printf 'batch.max_write_threads=16\n' > "${missing_yaml}/rest-server.properties" + + if output=$(HG_SERVER_CONF_DIR="${missing_rest}" \ + "${CODEX_DIR}/infra/patch-server-config.sh" --patch-only 2>&1); then + fail "patch unexpectedly accepted missing configuration files" + fi + assert_contains "${output}" "ERROR: missing ${missing_rest}/rest-server.properties" + + if output=$(HG_SERVER_CONF_DIR="${missing_yaml}" \ + "${CODEX_DIR}/infra/patch-server-config.sh" --patch-only 2>&1); then + fail "patch unexpectedly accepted a missing gremlin-server.yaml" + fi + assert_contains "${output}" "ERROR: missing ${missing_yaml}/gremlin-server.yaml" + grep -qx 'batch.max_write_threads=16' "${missing_yaml}/rest-server.properties" } test_environment_toml @@ -191,10 +178,8 @@ test_setup_dry_run test_stable_frontend_ports test_concurrent_frontend_claim test_shared_backend_runtime -test_owner_lock_contention -test_owner_lock_excludes_long_critical_section -test_server_repo_resolution test_compose_command_is_offline_and_layered test_server_config_patch +test_server_config_patch_requires_config_files echo "PASS: toolchain local environment tests"