Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 27 additions & 4 deletions scripts/ramdisk-orchestrator.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"
Expand Down Expand Up @@ -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
# ─────────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -497,6 +519,7 @@ generate_summary() {
# ─────────────────────────────────────────────────────────────────────
main() {
parse_args "$@"
resolve_mount_point

log "${BOLD}Hypatia RAM Disk Orchestrator v${VERSION}${NC}"

Expand Down
11 changes: 11 additions & 0 deletions tests/e2e.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
81 changes: 81 additions & 0 deletions tests/ramdisk-orchestrator-test.sh
Original file line number Diff line number Diff line change
@@ -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 <env...> -- <args...>: 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"
Loading