diff --git a/scripts/ramdisk-orchestrator.sh b/scripts/ramdisk-orchestrator.sh index 7bc07f91..fdcc628d 100755 --- a/scripts/ramdisk-orchestrator.sh +++ b/scripts/ramdisk-orchestrator.sh @@ -19,7 +19,9 @@ # --push Push committed fixes to remotes # --dry-run Show what would be done without doing it # --keep Do not unmount RAM disk on completion -# --mount PATH Custom mount point (default: per-user hypatia/workspace) +# --mount PATH Custom mount point (default: hypatia/workspace under +# $XDG_RUNTIME_DIR, else $XDG_STATE_HOME, else +# ~/.local/state) # --max-repos N Limit number of repos to process # --fallback-disk Fall back to Eclipse drive if RAM is insufficient # --quiet Suppress non-essential output @@ -43,8 +45,9 @@ FARM_DIR="${FARM_DIR:-${HYPATIA_DIR}/../.git-private-farm}" ECLIPSE_REPOS="${HYPATIA_REPOS_DIR:-$(dirname "$HYPATIA_DIR")}" # Defaults -WORKSPACE_BASE="${XDG_RUNTIME_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}}/hypatia" -MOUNT_POINT="${WORKSPACE_BASE}/workspace" +# Empty until parse_args has run: --help and --mount must work even when +# neither XDG variable nor HOME is set. See resolve_mount_point. +MOUNT_POINT="" RAMDISK_SIZE="2G" ORG="hyperpolymath" SEVERITY="high" @@ -103,6 +106,23 @@ parse_args() { done } +# The tmpfs mount point is re-found by later runs (stale-mount reuse), so it +# lives in a per-user directory, not a mktemp one. With no XDG directory and +# no HOME there is nowhere to put it: say so and stop, rather than let +# `set -u` die on an unbound HOME before --help can run. +resolve_mount_point() { + [[ -n "$MOUNT_POINT" ]] && return 0 + local base="${XDG_RUNTIME_DIR:-${XDG_STATE_HOME:-}}" + if [[ -z "$base" && -n "${HOME:-}" ]]; then + base="$HOME/.local/state" + fi + if [[ -z "$base" ]]; then + log_err "No XDG_RUNTIME_DIR, XDG_STATE_HOME or HOME; pass --mount PATH" + exit 1 + fi + MOUNT_POINT="$base/hypatia/workspace" +} + # ───────────────────────────────────────────────────────────────────── # Resource checks # ───────────────────────────────────────────────────────────────────── @@ -177,7 +197,9 @@ mount_workspace() { if ! sudo -n true 2>/dev/null; then log_warn "No passwordless sudo — falling back to disk workspace" USE_TMPFS=false - MOUNT_POINT="${WORKSPACE_BASE}/workspace-$$" + # Scratch space: XDG_RUNTIME_DIR is itself a small tmpfs, so + # use the system temp dir, with an unpredictable name. + MOUNT_POINT="$(mktemp -d -t hypatia-workspace.XXXXXXXX)" fi fi @@ -497,6 +519,7 @@ generate_summary() { # ───────────────────────────────────────────────────────────────────── main() { parse_args "$@" + resolve_mount_point log "${BOLD}Hypatia RAM Disk Orchestrator v${VERSION}${NC}" diff --git a/tests/e2e.sh b/tests/e2e.sh index f904e715..6c4d59f9 100644 --- a/tests/e2e.sh +++ b/tests/e2e.sh @@ -77,6 +77,17 @@ else fi echo "" +# ─── RAM disk orchestrator mount resolution ──────────────────────── +bold "RAM disk orchestrator mount resolution" + +if ramdisk_output="$(bash "${PROJECT_DIR}/tests/ramdisk-orchestrator-test.sh" 2>&1)"; then + pass "ramdisk-orchestrator resolves its mount point after parsing and refuses to run without one" +else + fail_test "ramdisk-orchestrator mount resolution" + printf '%s\n' "${ramdisk_output}" +fi +echo "" + # ─── Create test fixture ──────────────────────────────────────────── FIXTURE_DIR="$PROJECT_DIR/integration/fixtures/test-repo" if [ ! -d "$FIXTURE_DIR" ]; then diff --git a/tests/ramdisk-orchestrator-test.sh b/tests/ramdisk-orchestrator-test.sh new file mode 100755 index 00000000..91f34285 --- /dev/null +++ b/tests/ramdisk-orchestrator-test.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# Mount-point resolution in scripts/ramdisk-orchestrator.sh: the default is +# found after argument parsing, so --help and --mount work with no HOME and +# no XDG directory, and a run with nowhere to mount says so and stops. +# Every run is --dry-run with a repo name that exists nowhere: nothing is +# cloned, mounted or scanned. +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +script="$repo_root/scripts/ramdisk-orchestrator.sh" +fixture_root="$(mktemp -d /tmp/hypatia-ramdisk-test.XXXXXX)" +# cleanup removes the temporary fixture directory when its path matches the expected safe pattern. +cleanup() { + case "$fixture_root" in + /tmp/hypatia-ramdisk-test.*) rm -rf -- "$fixture_root" ;; + *) echo "refusing unsafe cleanup target: $fixture_root" >&2 ;; + esac +} +trap cleanup EXIT + +mkdir -p "$fixture_root/repos" "$fixture_root/run" "$fixture_root/state" +failures=0 + +# run_bare -- : the script in an environment holding only +# PATH, the given variables, and an empty repos directory; output in $out, +# status in $status. +run_bare() { + local vars=() + while [[ "$1" != "--" ]]; do vars+=("$1"); shift; done + shift + status=0 + out="$(env -i PATH="$PATH" HYPATIA_REPOS_DIR="$fixture_root/repos" "${vars[@]}" \ + bash "$script" "$@" 2>&1)" || status=$? +} + +check() { + local label="$1" ok="$2" + if [[ "$ok" == "true" ]]; then + echo "PASS: $label" + else + echo "FAIL: $label" + printf ' status %s, output:\n%s\n' "$status" "$out" | sed 's/^/ /' + failures=$((failures + 1)) + fi +} + +# --help needs no HOME. +run_bare -- --help +check "--help runs with no HOME or XDG directory" \ + "$([[ "$status" -eq 0 && "$out" == *"--mount PATH"* ]] && echo true || echo false)" + +# Nowhere to mount: a clear refusal naming the way out, not an unbound HOME. +run_bare -- --dry-run --size 1M no-such-repo +check "no HOME or XDG directory: refused, naming --mount" \ + "$([[ "$status" -eq 1 && "$out" == *"pass --mount PATH"* && "$out" != *"unbound variable"* ]] && echo true || echo false)" + +# --mount is enough on its own. +run_bare -- --dry-run --size 1M --mount "$fixture_root/m" no-such-repo +check "--mount with no HOME or XDG directory" \ + "$([[ "$status" -eq 0 && "$out" == *"Would mount tmpfs (1M) at $fixture_root/m"* ]] && echo true || echo false)" + +# The default follows XDG_RUNTIME_DIR, then XDG_STATE_HOME, then HOME. +run_bare XDG_RUNTIME_DIR="$fixture_root/run" XDG_STATE_HOME="$fixture_root/state" HOME="$fixture_root" -- \ + --dry-run --size 1M no-such-repo +check "default under XDG_RUNTIME_DIR" \ + "$([[ "$status" -eq 0 && "$out" == *"at $fixture_root/run/hypatia/workspace"* ]] && echo true || echo false)" + +run_bare XDG_STATE_HOME="$fixture_root/state" HOME="$fixture_root" -- --dry-run --size 1M no-such-repo +check "default under XDG_STATE_HOME" \ + "$([[ "$status" -eq 0 && "$out" == *"at $fixture_root/state/hypatia/workspace"* ]] && echo true || echo false)" + +run_bare HOME="$fixture_root" -- --dry-run --size 1M no-such-repo +check "default under HOME/.local/state" \ + "$([[ "$status" -eq 0 && "$out" == *"at $fixture_root/.local/state/hypatia/workspace"* ]] && echo true || echo false)" + +if [[ "$failures" -gt 0 ]]; then + echo "ramdisk-orchestrator-test: $failures failure(s)" + exit 1 +fi +echo "ramdisk-orchestrator-test: all passed"