Minecraft-Console-Client/.skills/mcc-integration-testing/scripts/run_achievements_test.sh
2026-03-30 17:25:08 +02:00

443 lines
13 KiB
Bash
Executable file

#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
# shellcheck source=tools/mcc-env.sh
source "$REPO_ROOT/tools/mcc-env.sh"
sed_in_place() {
if [[ "$(uname)" == "Darwin" ]]; then
sed -i '' "$@"
else
sed -i "$@"
fi
}
usage() {
cat <<'EOF'
Usage: run_achievements_test.sh [--no-build] <server-dir> <mc-version> <legacy|modern>
Examples:
.skills/mcc-integration-testing/scripts/run_achievements_test.sh --no-build 1.8 1.8 legacy
.skills/mcc-integration-testing/scripts/run_achievements_test.sh --no-build 1.21.11-Vanilla 1.21.11 modern
EOF
}
DO_BUILD=true
while [[ $# -gt 0 ]]; do
case "$1" in
--no-build) DO_BUILD=false; shift ;;
--build) DO_BUILD=true; shift ;;
-h|--help) usage; exit 0 ;;
*) break ;;
esac
done
if [[ $# -ne 3 ]]; then
usage >&2
exit 1
fi
SERVER_DIR="$1"
MC_VERSION="$2"
PROFILE="$3"
if [[ "$PROFILE" != "legacy" && "$PROFILE" != "modern" ]]; then
echo "Unsupported profile: $PROFILE" >&2
exit 1
fi
RUN_ROOT="${TMPDIR:-/tmp}/mcc-achievements"
RUN_ID="$(date +%Y%m%d-%H%M%S)"
RUN_DIR="$RUN_ROOT/$SERVER_DIR/$RUN_ID"
LATEST_LINK="$RUN_ROOT/$SERVER_DIR/latest"
MCC_LOG="$RUN_DIR/mcc.log"
BUILD_LOG="$RUN_DIR/build.log"
SERVER_TMUX_LOG="$RUN_DIR/server-tmux.log"
SERVER_FILE_LOG="$RUN_DIR/server-latest.log"
COMMAND_LOG="$RUN_DIR/commands.log"
SUMMARY_ENV="$RUN_DIR/summary.env"
PROBE_SCRIPT="$RUN_DIR/achievement_probe.cs"
CFG="$RUN_DIR/MinecraftClient.$MC_VERSION.ini"
INPUT_FILE="$REPO_ROOT/mcc_input.txt"
SERVER_LOG_FILE="$MCC_SERVERS/$SERVER_DIR/logs/latest.log"
TARGET_ID="minecraft:story/root"
TARGET_COMMAND_GRANT="advancement grant CursorBot only minecraft:story/root"
TARGET_COMMAND_REVOKE="advancement revoke CursorBot only minecraft:story/root"
TARGET_TYPE="Modern 🌱"
PORT="unknown"
MCC_PID=""
INITIAL_STATUS="❌"
GRANT_STATUS="❌"
REVOKE_STATUS="❌"
API_STATUS="❌"
VERDICT="❌ Fail"
NOTE="Run did not complete."
EXECUTED="yes"
if [[ "$PROFILE" == "legacy" ]]; then
TARGET_ID="achievement.openInventory"
TARGET_COMMAND_GRANT="achievement give achievement.openInventory CursorBot"
TARGET_COMMAND_REVOKE="achievement take achievement.openInventory CursorBot"
TARGET_TYPE="Legacy 🧱"
fi
mkdir -p "$RUN_DIR"
write_summary() {
{
printf 'VERSION=%q\n' "$MC_VERSION"
printf 'SERVER_DIR=%q\n' "$SERVER_DIR"
printf 'PROFILE=%q\n' "$PROFILE"
printf 'FAMILY=%q\n' "$TARGET_TYPE"
printf 'PORT=%q\n' "$PORT"
printf 'RUN_DIR=%q\n' "$RUN_DIR"
printf 'MCC_LOG=%q\n' "$MCC_LOG"
printf 'SERVER_LOG=%q\n' "$RUN_DIR/server-latest.log"
printf 'SERVER_FILE_LOG=%q\n' "$SERVER_LOG_FILE"
printf 'SERVER_TMUX_LOG=%q\n' "$SERVER_TMUX_LOG"
printf 'COPIED_SERVER_LOG=%q\n' "$RUN_DIR/server-latest.log"
printf 'COMMAND_LOG=%q\n' "$COMMAND_LOG"
printf 'SUMMARY_ENV=%q\n' "$SUMMARY_ENV"
printf 'TARGET_ID=%q\n' "$TARGET_ID"
printf 'INITIAL_STATUS=%q\n' "$INITIAL_STATUS"
printf 'GRANT_STATUS=%q\n' "$GRANT_STATUS"
printf 'REVOKE_STATUS=%q\n' "$REVOKE_STATUS"
printf 'API_STATUS=%q\n' "$API_STATUS"
printf 'VERDICT=%q\n' "$VERDICT"
printf 'NOTE=%q\n' "$NOTE"
printf 'EXECUTED=%q\n' "$EXECUTED"
} > "$SUMMARY_ENV"
}
capture_server_logs() {
mc-log "$SERVER_DIR" 400 > "$SERVER_TMUX_LOG" 2>/dev/null || true
if [[ -f "$SERVER_LOG_FILE" ]]; then
cp "$SERVER_LOG_FILE" "$RUN_DIR/server-latest.log" 2>/dev/null || true
fi
}
cleanup() {
capture_server_logs
if [[ -n "${MCC_PID:-}" ]] && kill -0 "$MCC_PID" 2>/dev/null; then
echo "quit" >> "$INPUT_FILE" 2>/dev/null || true
sleep 2
kill "$MCC_PID" 2>/dev/null || true
wait "$MCC_PID" 2>/dev/null || true
fi
mc-stop "$SERVER_DIR" >/dev/null 2>&1 || true
ln -sfn "$RUN_DIR" "$LATEST_LINK"
write_summary
}
trap cleanup EXIT
log_step() {
printf '[%s] %s\n' "$(date '+%H:%M:%S')" "$1" | tee -a "$COMMAND_LOG"
}
fail() {
NOTE="$1"
VERDICT="❌ Fail"
exit 1
}
wait_for_file_pattern() {
local file="$1"
local pattern="$2"
local description="$3"
local timeout="${4:-60}"
local elapsed=0
while (( elapsed < timeout )); do
if [[ -f "$file" ]] && grep -Fq "$pattern" "$file"; then
return 0
fi
sleep 1
((elapsed += 1))
done
echo "Timed out waiting for: $description" >&2
return 1
}
wait_for_server_ready() {
local timeout="${1:-60}"
local elapsed=0
while (( elapsed < timeout )); do
if mc-log "$SERVER_DIR" 250 2>/dev/null | grep -Fq "Done ("; then
return 0
fi
sleep 1
((elapsed += 1))
done
echo "Timed out waiting for server readiness" >&2
return 1
}
disable_noisy_bots() {
sed_in_place '/^\[ChatBot.ScriptScheduler\]/,/^\[/ { s/^Enabled = true/Enabled = false/; }' "$REPO_ROOT/MinecraftClient.ini"
sed_in_place '/^\[ChatBot.DiscordRpc\]/,/^\[/ { s/^Enabled = true/Enabled = false/; }' "$REPO_ROOT/MinecraftClient.ini"
sed_in_place '/^\[ChatBot.AntiAFK\]/,/^\[/ { s/^Enabled = true/Enabled = false/; }' "$REPO_ROOT/MinecraftClient.ini"
sed_in_place '/^\[ChatBot.AutoDig\]/,/^\[/ { s/^Enabled = true/Enabled = false/; }' "$REPO_ROOT/MinecraftClient.ini"
sed_in_place '/^\[ChatBot.AutoAttack\]/,/^\[/ { s/^Enabled = true/Enabled = false/; }' "$REPO_ROOT/MinecraftClient.ini"
sed_in_place '/^\[ChatBot.PlayerListLogger\]/,/^\[/ { s/^Enabled = true/Enabled = false/; }' "$REPO_ROOT/MinecraftClient.ini"
sed_in_place '/^\[ChatBot.ReplayCapture\]/,/^\[/ { s/^Enabled = true/Enabled = false/; }' "$REPO_ROOT/MinecraftClient.ini"
}
ensure_root_config() {
if [[ -f "$REPO_ROOT/MinecraftClient.ini" ]]; then
return
fi
(
cd "$REPO_ROOT"
dotnet run --project MinecraftClient -c Release --no-build -- --help >/dev/null 2>&1
)
}
write_probe_script() {
cat > "$PROBE_SCRIPT" <<EOF
//MCCScript 1.0
MCC.LoadBot(new AchievementProbeBot());
//MCCScript Extensions
public class AchievementProbeBot : ChatBot
{
private const string TargetId = "$TARGET_ID";
public override void Initialize()
{
LogToConsole("[ACH_TEST] probe initialized");
DumpState("initialize");
}
public override void AfterGameJoined()
{
LogToConsole("[ACH_TEST] after join");
DumpState("after_join");
}
public override void OnAchievementUpdate(IReadOnlyList<Achievement> updated, IReadOnlyList<string> removedIds, bool reset)
{
LogToConsole($"[ACH_TEST] event reset={reset} updated={updated.Count} removed={removedIds.Count}");
DumpState("event");
}
private void DumpState(string origin)
{
Achievement[] all = GetAchievements();
Achievement[] unlocked = GetUnlockedAchievements();
Achievement[] locked = GetLockedAchievements();
Achievement? target = null;
foreach (Achievement entry in all)
{
if (entry.Id == TargetId)
{
target = entry;
break;
}
}
string titleState = "missing";
string completionState = "missing";
if (target is not null)
{
titleState = target.Title is null ? "null" : "present";
completionState = target.IsCompleted ? "done" : "todo";
}
LogToConsole($"[ACH_TEST] snapshot origin={origin} all={all.Length} unlocked={unlocked.Length} locked={locked.Length}");
LogToConsole($"[ACH_TEST] target_state origin={origin} id={TargetId} title={titleState} completed={completionState}");
}
}
EOF
}
run_server_command() {
local cmd="$1"
local attempt
log_step "SERVER> $cmd"
for attempt in 1 2 3 4 5; do
if mc-rcon "$cmd" >/dev/null 2>&1; then
sleep 1
return 0
fi
sleep 1
done
fail "Server command failed: $cmd"
}
run_mcc_command() {
local name="$1"
local cmd="$2"
local delay="${3:-2}"
local start_line=0
local end_line=0
if [[ -f "$MCC_LOG" ]]; then
start_line="$(wc -l < "$MCC_LOG")"
fi
log_step "MCC> $cmd"
echo "$cmd" >> "$INPUT_FILE"
sleep "$delay"
if [[ -f "$MCC_LOG" ]]; then
end_line="$(wc -l < "$MCC_LOG")"
fi
if (( end_line > start_line )); then
sed -n "$((start_line + 1)),$((end_line))p" "$MCC_LOG" > "$RUN_DIR/$name.mcc.log"
else
: > "$RUN_DIR/$name.mcc.log"
fi
}
assert_pattern() {
local file="$1"
local pattern="$2"
local description="$3"
grep -Fq "$pattern" "$file" || fail "$description"
}
if ! command -v java >/dev/null 2>&1 || ! java -version >/dev/null 2>&1; then
fail "java was not found on PATH."
fi
if ! command -v tmux >/dev/null 2>&1; then
fail "tmux was not found on PATH."
fi
if [[ ! -d "$MCC_SERVERS/$SERVER_DIR" ]]; then
fail "Server directory not found: $MCC_SERVERS/$SERVER_DIR"
fi
PORT="$(bash "$SCRIPT_DIR/get_server_port.sh" "$SERVER_DIR")"
ensure_root_config
"$SCRIPT_DIR/ensure_offline_server.sh" "$SERVER_DIR"
disable_noisy_bots
write_probe_script
if [[ "$PROFILE" == "legacy" && -f "$MCC_SERVERS/$SERVER_DIR/server.properties" ]]; then
sed_in_place 's/^use-native-transport=.*/use-native-transport=false/' "$MCC_SERVERS/$SERVER_DIR/server.properties"
fi
if $DO_BUILD; then
log_step "BUILD> dotnet build MinecraftClient.sln -c Release"
mcc-build > "$BUILD_LOG" 2>&1 || fail "dotnet build failed."
else
: > "$BUILD_LOG"
fi
: > "$INPUT_FILE"
rm -f "$MCC_LOG"
log_step "Starting server $SERVER_DIR on port $PORT"
mc-start "$SERVER_DIR" >/dev/null
wait_for_server_ready || fail "Server did not become ready."
log_step "Starting MCC for $MC_VERSION"
(
cd "$REPO_ROOT"
MCC_FILE_INPUT=1 dotnet run --project MinecraftClient -c Release --no-build -- \
CursorBot \
- \
"localhost:$PORT" \
"--accounttype=mojang" \
"--minecraftversion=$MC_VERSION" \
"--terrainandmovements=true" \
"--inventoryhandling=true" \
"--entityhandling=true" \
"--autorespawn=true" \
"--debugmessages=true" \
> "$MCC_LOG" 2>&1
) &
MCC_PID=$!
wait_for_file_pattern "$MCC_LOG" "Server was successfully joined." "MCC join success" 90 || fail "MCC failed to join."
wait_for_file_pattern "$SERVER_LOG_FILE" "CursorBot joined the game" "server join entry" 30 || fail "Server never logged the join."
run_server_command "op CursorBot"
run_server_command "gamerule sendCommandFeedback true"
if [[ "$PROFILE" == "modern" ]]; then
run_server_command "gamerule logAdminCommands true"
fi
run_server_command "time set day"
run_server_command "weather clear"
run_mcc_command "load_probe" "script $PROBE_SCRIPT" 3
wait_for_file_pattern "$MCC_LOG" "[ACH_TEST] probe initialized" "probe startup" 30 || fail "Probe script did not initialize."
run_mcc_command "baseline_debug" "debug state" 2
run_mcc_command "baseline_all" "achievement" 2
run_mcc_command "baseline_locked" "achievement locked" 2
run_mcc_command "baseline_unlocked" "achievement unlocked" 2
run_server_command "$TARGET_COMMAND_GRANT"
sleep 3
run_mcc_command "after_grant_all" "achievement" 2
run_mcc_command "after_grant_unlocked" "achievement unlocked" 2
run_server_command "$TARGET_COMMAND_REVOKE"
sleep 3
run_mcc_command "after_revoke_all" "achievement" 2
run_mcc_command "after_revoke_locked" "achievement locked" 2
assert_pattern "$MCC_LOG" "Achievements/Advancements:" "Achievement command header never appeared."
if ! grep -Fq "No achievements/advancements received yet." "$RUN_DIR/baseline_all.mcc.log"; then
INITIAL_STATUS="✅"
fi
if grep -Fq "$TARGET_ID" "$RUN_DIR/after_grant_unlocked.mcc.log" && grep -Fq "[DONE]" "$RUN_DIR/after_grant_unlocked.mcc.log"; then
GRANT_STATUS="✅"
fi
if [[ "$PROFILE" == "legacy" ]]; then
if grep -Fq "$TARGET_ID" "$RUN_DIR/after_revoke_locked.mcc.log" && grep -Fq "[TODO]" "$RUN_DIR/after_revoke_locked.mcc.log"; then
REVOKE_STATUS="✅"
fi
else
if grep -Fq "$TARGET_ID" "$RUN_DIR/after_revoke_locked.mcc.log" && grep -Fq "[TODO]" "$RUN_DIR/after_revoke_locked.mcc.log"; then
REVOKE_STATUS="✅"
elif [[ "$GRANT_STATUS" == "✅" ]] && ! grep -Fq "$TARGET_ID" "$RUN_DIR/after_revoke_all.mcc.log"; then
REVOKE_STATUS="✅"
fi
fi
if grep -Fq "[ACH_TEST] event" "$MCC_LOG" && grep -Fq "target_state origin=event id=$TARGET_ID title=" "$MCC_LOG"; then
API_STATUS="✅"
fi
case "$INITIAL_STATUS|$GRANT_STATUS|$REVOKE_STATUS|$API_STATUS" in
"✅|✅|✅|✅")
VERDICT="✅ Pass"
NOTE="All planned achievement checks passed."
;;
*"✅"*)
VERDICT="⚠️ Partial"
NOTE="At least one achievement phase passed, but the matrix did not fully clear."
;;
*)
VERDICT="❌ Fail"
NOTE="Achievement checks did not produce the expected evidence."
;;
esac
run_mcc_command "quit" "quit" 2
NOTE="$NOTE Artifacts saved in $RUN_DIR."