Merge pull request #3077 from BruceChenQAQ/bruce/shared-server-mcc-sessions

tools: isolate MCC sessions while sharing local servers
This commit is contained in:
BruceChen 2026-04-12 23:51:25 +08:00 committed by GitHub
commit 248a7a2f6f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 2547 additions and 276 deletions

View file

@ -12,7 +12,7 @@ Use this skill when the task needs a real local server loop, not just code readi
- Solution: `MinecraftClient.sln`
- Runtime target: `.NET 10` / `net10.0`
- Environment: Linux, macOS, or WSL with Java, tmux, python3, and dotnet available
- Default server root: `${MCC_SERVERS:-$MCC_REPO/MinecraftOfficial/downloads}`
- Default server root after `source tools/mcc-env.sh`: `${MCC_SERVERS:-<repo>/MinecraftOfficial/downloads}`
- Default validation target when the user does not specify a version: `1.21.11`
## Console modes
@ -34,6 +34,50 @@ Both modes support the same commands and input/output through `ConsoleIO.Backend
- A server log line containing `Done (` means startup finished. It does not guarantee that RCON is ready on the first attempt. Retry early `mc-rcon` commands.
- When instructions, docs, and code disagree, trust current code and current tool behavior first.
## Shared server, isolated MCC sessions
- `mc-*` commands operate on the shared local Minecraft server.
- `mcc-*` commands operate on one MCC client session.
- The default `session` is the current worktree name.
- The default username is derived from `session`, unless you pass `--username`.
- Session files live under `${TMPDIR:-/tmp}/mcc-debug/<session>/`.
- `MCC_SERVERS` stays the shared server-root override.
Keep shared servers running by default. Do not stop or reset them unless the user explicitly asks for that, or you need to switch server versions.
Two worktrees can debug against one shared server like this:
```bash
# worktree A
cd ~/Minecraft/Minecraft-Console-Client
source tools/mcc-env.sh
mc-start 1.21.11
mcc-debug -v 1.21.11 --file-input
# worktree B
cd ~/Minecraft/Minecraft-Console-Client-foo
source tools/mcc-env.sh
mcc-debug -v 1.21.11 --file-input
# from each worktree, mcc-* targets that worktree's default session
mcc-state
```
If you want two MCC sessions from the same worktree, pass `--session NAME` explicitly.
## tmpfs build mode
Use this on machines with enough RAM when you want worktree-isolated builds outside the repo tree:
```bash
source tools/mcc-env.sh
export MCC_BUILD_MODE=tmpfs
mcc-build
mcc-build-clean
```
`MCC_BUILD_MODE=tmpfs` redirects build output to `/dev/shm/mcc-build/<worktree>/` on Linux, or `${TMPDIR:-/tmp}/mcc-build/<worktree>/` if `/dev/shm` is unavailable.
## Preflight and reset
Before scripted runs, especially on macOS or in a reused tmux environment:
@ -58,9 +102,11 @@ Interactive shell:
```bash
source tools/mcc-env.sh
SESSION="$(_mcc_resolve_session)"
USERNAME="$(_mcc_resolve_username "$SESSION")"
mc-start 1.21.11
mc-log 1.21.11 100
mc-rcon "op CursorBot"
mc-rcon "op $USERNAME"
mc-stop 1.21.11
```
@ -68,7 +114,7 @@ Non-interactive shell:
```bash
tools/start-server.sh 1.21.11
tools/mc-rcon.sh "op CursorBot"
tools/mc-rcon.sh "op mcc_smoke_a"
```
If the servers live outside the repo, set `MCC_SERVERS` before sourcing or invoking the tools:
@ -104,16 +150,16 @@ mcc-debug -v 1.21.11 --file-input --no-build
### What mcc-debug.sh does
1. Builds MCC (unless `--no-build`)
2. Creates a clean temp config at `/tmp/mcc-debug/MinecraftClient.debug.ini` with CursorBot account, Terrain/Inventory/Entity enabled and noisy bots disabled
2. Creates a clean temp config at `${TMPDIR:-/tmp}/mcc-debug/<session>/MinecraftClient.debug.ini`
3. Ensures server is running (starts if not, waits for `Done (`)
4. Launches MCC in the specified mode
4. Launches MCC in a session-scoped tmux session and session-scoped log/input/pid files
### After launch
- **FileInput mode**: drive MCC via `mcc-cmd "debug state"` or `echo "debug state" >> mcc_input.txt`
- **Interactive/TUI mode**: attach with `tmux attach -t mcc-debug`, type commands directly
- **Logs**: `tail -f /tmp/mcc-debug/mcc-debug.log` (FileInput mode only; TUI/interactive mode outputs to tmux)
- **Server RCON**: `mc-rcon "op CursorBot"`, `mc-rcon "gamemode creative CursorBot"`
- **FileInput mode**: drive MCC via `mcc-cmd --session smoke-a "debug state"`, or just `mcc-cmd "debug state"` from the same worktree
- **Interactive/TUI mode**: attach with `tmux attach -t mcc-<session>`
- **Logs**: `mcc-log-mcc --session smoke-a` or `tail -f "${TMPDIR:-/tmp}/mcc-debug/<session>/mcc-debug.log"`
- **Server RCON**: grant op or gamemode to the username derived from that session
## Debug commands (in-game)
@ -128,7 +174,7 @@ Prints a one-shot summary of MCC's internal state:
```
=== MCC Debug State ===
Server: localhost:25565
Username: CursorBot
Username: mcc_smoke_a
Protocol: 774
GameMode: 1
Health: 20.0
@ -152,18 +198,20 @@ For agents calling MCC commands programmatically:
```bash
source tools/mcc-env.sh
mcc-debug -v 1.21.11 --file-input --no-build
SESSION="smoke-a"
mcc-debug -v 1.21.11 --file-input --session "$SESSION" --no-build
# Send commands:
mcc-cmd "debug state"
mcc-cmd "inventory player list"
mcc-cmd "entity"
mcc-cmd --session "$SESSION" "debug state"
mcc-cmd --session "$SESSION" "inventory player list"
mcc-cmd --session "$SESSION" "entity"
# Check results:
tail -20 /tmp/mcc-debug/mcc-debug.log
mcc-log-mcc --session "$SESSION"
# Stop:
mcc-cmd "quit"
mcc-cmd --session "$SESSION" "quit"
mcc-kill --session "$SESSION"
mc-stop 1.21.11
```
@ -171,10 +219,11 @@ mc-stop 1.21.11
```bash
source tools/mcc-env.sh
mcc-debug -v 1.21.11
SESSION="live-a"
mcc-debug -v 1.21.11 --session "$SESSION"
# In another terminal:
tmux attach -t mcc-debug
tmux attach -t "mcc-$SESSION"
# Type commands directly in MCC console
```
@ -192,22 +241,23 @@ TUI mode runs Consolonia full-screen in a tmux session. Key differences:
```bash
source tools/mcc-env.sh
mcc-debug -v 1.21.11 -m tui --no-build
SESSION="tui-a"
mcc-debug -v 1.21.11 -m tui --session "$SESSION" --no-build
# Cannot use mcc-cmd (no FileInput); must use tmux send-keys:
tmux send-keys -t mcc-debug "/debug state" Enter
tmux send-keys -t "mcc-$SESSION" "/debug state" Enter
# Read TUI screen:
tmux capture-pane -t mcc-debug -p -S -30
tmux capture-pane -t "mcc-$SESSION" -p -S -30
# Stop:
tmux send-keys -t mcc-debug Escape
tmux send-keys -t "mcc-$SESSION" Escape
```
**Caveat with tmux send-keys and Consolonia**: When sending text containing `/`, the Enter key may need to be sent separately:
```bash
tmux send-keys -t mcc-debug "/inventory player list"
tmux send-keys -t mcc-debug Enter
tmux send-keys -t "mcc-$SESSION" "/inventory player list"
tmux send-keys -t "mcc-$SESSION" Enter
```
## mcc-env.sh quick reference
@ -226,26 +276,28 @@ After `source tools/mcc-env.sh`:
| `mc-wait-stop VER [SEC]` | Wait for server shutdown, with force-kill fallback |
| `mc-reset-test-env [--all|VER...]` | Reset shared tmux server state and stale pipes |
| `mcc-build` | Build MCC |
| `mcc-run [PORT]` | Run MCC classic+FileInput on port |
| `mcc-tui [PORT]` | Run MCC TUI mode in tmux |
| `mcc-cmd "CMD"` | Append command to mcc_input.txt |
| `mcc-kill` | Kill MCC process and debug session |
| `mcc-build-clean` | Clear the current worktree's build output |
| `mcc-run [--session NAME] [--username NAME] [--port PORT]` | Convenience wrapper for `mcc-debug --file-input --no-build` |
| `mcc-tui [--session NAME] [--username NAME] [--port PORT]` | Convenience wrapper for `mcc-debug -m tui --no-build` |
| `mcc-cmd [--session NAME] "CMD"` | Append a command to one session's input file |
| `mcc-kill [--session NAME]` | Kill one MCC process and session |
| `mcc-debug [OPTS]` | One-step debug session (see above) |
| `mcc-log-mcc` | Tail MCC debug log |
| `mcc-state` | Send `debug state` and print last 30 log lines |
| `mcc-log-mcc [--session NAME]` | Tail one MCC debug log |
| `mcc-state [--session NAME]` | Send `debug state` and print the last 30 log lines |
| `mcc-preflight [VER...]` | Verify Java, tmux, dotnet, python3, and server dirs |
## Temporary config recipe
```bash
source tools/mcc-env.sh
TEST_ROOT="${TMPDIR:-/tmp}/mcc-dev"
CFG="$TEST_ROOT/MinecraftClient.1.21.11.ini"
mkdir -p "$TEST_ROOT"
SESSION="smoke-a"
USERNAME="$(_mcc_resolve_username "$SESSION")"
CFG="$(_mcc_session_root "$SESSION")/MinecraftClient.debug.ini"
mkdir -p "$(_mcc_session_root "$SESSION")"
bash "$MCC_REPO/.skills/mcc-integration-testing/scripts/prepare_offline_mcc_config.sh" \
"$CFG" \
"1.21.11" \
"CursorBot"
"$USERNAME"
```
For TUI mode, also add:
@ -259,14 +311,14 @@ MCC output should include:
- `[MCC] Server was successfully joined.`
Server output should include:
Server output should include the session-derived username, for example:
- `CursorBot joined the game`
- `mcc_smoke_a joined the game`
Basic command check:
```bash
mcc-cmd "inventory player list"
mcc-cmd --session smoke-a "inventory player list"
```
If a scripted run fails before MCC joins, check for a harness problem before assuming a product regression. Missing `mcc.log`, a pre-join `Connection refused`, or a server that never reached `Done (` usually means shared-state cleanup or startup failed.
@ -292,7 +344,7 @@ If a scripted run fails before MCC joins, check for a harness problem before ass
- Legacy `1.8` and `1.8.9` servers may need `use-native-transport=false` in `server.properties` on some Linux environments.
- For timing-sensitive work, do not trust wall-clock intuition. Use a real server run and capture evidence from logs or test scripts.
- **TUI mode tip**: if the terminal becomes unresponsive after a crash, run `stty sane && reset` to restore it.
- **tmux capture trick**: `tmux capture-pane -t mcc-debug -p -S -50` captures the last 50 lines of a tmux session without attaching.
- **tmux capture trick**: `tmux capture-pane -t mcc-<session> -p -S -50` captures the last 50 lines of a tmux session without attaching.
## Tool files

View file

@ -19,7 +19,7 @@ This skill uses a fixed set of stable commands for local offline integration tes
- `look east`
- `/gamemode survival`
- `respawn`
- `/tp CursorBot 0 -60 0`
- `/tp MCCBot 0 -60 0`
- `smoke_test_from_mcc_full_spectrum`
- `integration_test_chat_response`
@ -29,63 +29,63 @@ Notes:
## Server-side commands via `mc-rcon`
- `op CursorBot`
- `op MCCBot`
- `gamerule sendCommandFeedback true`
- `gamerule logAdminCommands true`
- `time set day`
- `weather clear`
- `say Hello from the server console`
- `msg CursorBot This is a private whisper`
- `effect give CursorBot minecraft:speed 30 1`
- `effect give CursorBot minecraft:regeneration 10 1`
- `kill CursorBot`
- `msg MCCBot This is a private whisper`
- `effect give MCCBot minecraft:speed 30 1`
- `effect give MCCBot minecraft:regeneration 10 1`
- `kill MCCBot`
## Representative entity coverage
- `execute as CursorBot at @s run summon minecraft:cow ~2 ~ ~`
- `execute as CursorBot at @s run summon minecraft:zombie ~4 ~ ~`
- `execute as CursorBot at @s run summon minecraft:creeper ~6 ~ ~`
- `execute as CursorBot at @s run summon minecraft:skeleton ~8 ~ ~`
- `execute as CursorBot at @s run summon minecraft:villager ~-2 ~ ~`
- `execute as CursorBot at @s run summon minecraft:allay ~-4 ~ ~`
- `execute as CursorBot at @s run summon minecraft:armor_stand ~ ~ ~2`
- `execute as CursorBot at @s run summon minecraft:item_display ~-6 ~ ~ {item:{id:"minecraft:diamond",count:1}}`
- `execute as CursorBot at @s run summon minecraft:spider ~10 ~ ~`
- `execute as CursorBot at @s run summon minecraft:pig ~-8 ~ ~`
- `execute as MCCBot at @s run summon minecraft:cow ~2 ~ ~`
- `execute as MCCBot at @s run summon minecraft:zombie ~4 ~ ~`
- `execute as MCCBot at @s run summon minecraft:creeper ~6 ~ ~`
- `execute as MCCBot at @s run summon minecraft:skeleton ~8 ~ ~`
- `execute as MCCBot at @s run summon minecraft:villager ~-2 ~ ~`
- `execute as MCCBot at @s run summon minecraft:allay ~-4 ~ ~`
- `execute as MCCBot at @s run summon minecraft:armor_stand ~ ~ ~2`
- `execute as MCCBot at @s run summon minecraft:item_display ~-6 ~ ~ {item:{id:"minecraft:diamond",count:1}}`
- `execute as MCCBot at @s run summon minecraft:spider ~10 ~ ~`
- `execute as MCCBot at @s run summon minecraft:pig ~-8 ~ ~`
## Block placement coverage
- `execute as CursorBot at @s run fill ~1 ~ ~1 ~3 ~2 ~3 minecraft:stone`
- `execute as CursorBot at @s run setblock ~5 ~ ~5 minecraft:chest`
- `execute as CursorBot at @s run setblock ~5 ~1 ~5 minecraft:furnace`
- `execute as CursorBot at @s run setblock ~6 ~ ~5 minecraft:crafting_table`
- `execute as MCCBot at @s run fill ~1 ~ ~1 ~3 ~2 ~3 minecraft:stone`
- `execute as MCCBot at @s run setblock ~5 ~ ~5 minecraft:chest`
- `execute as MCCBot at @s run setblock ~5 ~1 ~5 minecraft:furnace`
- `execute as MCCBot at @s run setblock ~6 ~ ~5 minecraft:crafting_table`
## Dimension change coverage
- `execute in minecraft:the_nether run tp CursorBot 0 64 0`
- `execute in minecraft:overworld run tp CursorBot 0 -60 0`
- `execute in minecraft:the_nether run tp MCCBot 0 64 0`
- `execute in minecraft:overworld run tp MCCBot 0 -60 0`
## Representative particle coverage
- `execute as CursorBot at @s run particle minecraft:happy_villager ~ ~1 ~ 0.5 0.5 0.5 0 12 force`
- `execute as CursorBot at @s run particle minecraft:end_rod ~ ~1 ~ 0.5 0.5 0.5 0.01 20 force`
- `execute as CursorBot at @s run particle minecraft:explosion ~ ~1 ~ 0 0 0 0 1 force`
- `execute as CursorBot at @s run particle minecraft:totem_of_undying ~ ~1 ~ 0.5 0.5 0.5 0.1 20 force`
- `execute as CursorBot at @s run particle minecraft:flame ~ ~1 ~ 0.2 0.2 0.2 0.02 30 force`
- `execute as CursorBot at @s run particle minecraft:heart ~ ~2 ~ 0.3 0.3 0.3 0 5 force`
- `execute as MCCBot at @s run particle minecraft:happy_villager ~ ~1 ~ 0.5 0.5 0.5 0 12 force`
- `execute as MCCBot at @s run particle minecraft:end_rod ~ ~1 ~ 0.5 0.5 0.5 0.01 20 force`
- `execute as MCCBot at @s run particle minecraft:explosion ~ ~1 ~ 0 0 0 0 1 force`
- `execute as MCCBot at @s run particle minecraft:totem_of_undying ~ ~1 ~ 0.5 0.5 0.5 0.1 20 force`
- `execute as MCCBot at @s run particle minecraft:flame ~ ~1 ~ 0.2 0.2 0.2 0.02 30 force`
- `execute as MCCBot at @s run particle minecraft:heart ~ ~2 ~ 0.3 0.3 0.3 0 5 force`
## Representative sound coverage
- `execute as CursorBot at @s run playsound minecraft:entity.lightning_bolt.thunder master CursorBot ~ ~ ~ 1 1 0`
- `execute as CursorBot at @s run playsound minecraft:block.note_block.bell master CursorBot ~ ~ ~ 1 1 0`
- `execute as CursorBot at @s run playsound minecraft:entity.experience_orb.pickup master CursorBot ~ ~ ~ 1 1 0`
- `execute as MCCBot at @s run playsound minecraft:entity.lightning_bolt.thunder master MCCBot ~ ~ ~ 1 1 0`
- `execute as MCCBot at @s run playsound minecraft:block.note_block.bell master MCCBot ~ ~ ~ 1 1 0`
- `execute as MCCBot at @s run playsound minecraft:entity.experience_orb.pickup master MCCBot ~ ~ ~ 1 1 0`
## Explosion coverage
- `execute as CursorBot at @s run summon minecraft:tnt ~3 ~ ~`
- `execute as CursorBot at @s run summon minecraft:tnt ~6 ~ ~`
- `execute as MCCBot at @s run summon minecraft:tnt ~3 ~ ~`
- `execute as MCCBot at @s run summon minecraft:tnt ~6 ~ ~`
## Kill and respawn cycle
- `kill CursorBot` (via RCON, requires survival mode)
- `kill MCCBot` (via RCON, requires survival mode)
- `respawn` (via MCC command after death)

View file

@ -73,7 +73,7 @@ wait_for_server_stop() {
((elapsed += 1))
done
mc-kill "$version" >/dev/null 2>&1 || true
mc-kill "$version" --confirm >/dev/null 2>&1 || true
if ! server_running "$version"; then
return 0
@ -104,7 +104,7 @@ remove_stale_stdin_pipe() {
local version="$1"
local pipe_path="$MCC_SERVERS/$version/stdin.pipe"
if [[ -e "$pipe_path" && ! -p "$pipe_path" ]]; then
if [[ -e "$pipe_path" ]] && ! server_running "$version"; then
rm -f "$pipe_path"
fi
}

View file

@ -41,12 +41,12 @@ upsert_property() {
if [[ ! -f "$PROPS_FILE" ]]; then
mc-start "$VERSION"
wait_for_server_ready "$VERSION"
mc-stop "$VERSION"
mc-stop "$VERSION" --confirm
wait_for_server_stop "$VERSION"
fi
if server_running; then
mc-stop "$VERSION"
mc-stop "$VERSION" --confirm
wait_for_server_stop "$VERSION"
fi

View file

@ -28,11 +28,11 @@ if [[ $# -ge 3 && -f "$1" ]]; then
TEMPLATE_INI="$1"
OUTPUT_INI="$2"
MC_VERSION="$3"
LOGIN_NAME="${4:-CursorBot}"
LOGIN_NAME="${4:-MCCBot}"
else
OUTPUT_INI="$1"
MC_VERSION="$2"
LOGIN_NAME="${3:-CursorBot}"
LOGIN_NAME="${3:-MCCBot}"
fi
ACCOUNT_TYPE="${MCC_TEST_ACCOUNT_TYPE:-mojang}"

View file

@ -12,7 +12,7 @@ usage() {
cat <<'EOF'
Usage: reset_shared_test_state.sh [--all | <server-dir>...]
Kills shared tmux test sessions and removes stale stdin pipes.
Kills shared server tmux test sessions and removes stale server stdin pipes.
EOF
}
@ -26,8 +26,6 @@ kill_named_session() {
tmux kill-session -t "$session_name" 2>/dev/null || true
}
kill_named_session "mcc-debug"
if [[ $# -eq 0 || "${1:-}" == "--all" ]]; then
while IFS= read -r session_name; do
[[ -z "$session_name" ]] && continue
@ -36,9 +34,7 @@ if [[ $# -eq 0 || "${1:-}" == "--all" ]]; then
while IFS= read -r pipe_path; do
[[ -z "$pipe_path" ]] && continue
if [[ ! -p "$pipe_path" ]]; then
rm -f "$pipe_path"
fi
rm -f "$pipe_path"
done < <(find "$MCC_SERVERS" -maxdepth 2 -name 'stdin.pipe' 2>/dev/null || true)
else
for version in "$@"; do
@ -46,5 +42,3 @@ else
remove_stale_stdin_pipe "$version"
done
fi
rm -f "$MCC_REPO/mcc_input.txt"

View file

@ -37,6 +37,8 @@ fi
SERVER_DIR="$1"
MC_VERSION="$2"
PROFILE="$3"
SESSION_NAME="achievements-${SERVER_DIR//[^a-zA-Z0-9]/_}-${PROFILE}"
TEST_USERNAME="$(_mcc_resolve_username "$SESSION_NAME")"
if [[ "$PROFILE" != "legacy" && "$PROFILE" != "modern" ]]; then
echo "Unsupported profile: $PROFILE" >&2
@ -55,11 +57,11 @@ 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"
INPUT_FILE="$(_mcc_session_input_file "$SESSION_NAME")"
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_COMMAND_GRANT="advancement grant $TEST_USERNAME only minecraft:story/root"
TARGET_COMMAND_REVOKE="advancement revoke $TEST_USERNAME only minecraft:story/root"
TARGET_TYPE="Modern 🌱"
PORT="unknown"
MCC_PID=""
@ -74,8 +76,8 @@ 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_COMMAND_GRANT="achievement give achievement.openInventory $TEST_USERNAME"
TARGET_COMMAND_REVOKE="achievement take achievement.openInventory $TEST_USERNAME"
TARGET_TYPE="Legacy 🧱"
fi
@ -124,7 +126,7 @@ cleanup() {
wait "$MCC_PID" 2>/dev/null || true
fi
mc-stop "$SERVER_DIR" >/dev/null 2>&1 || true
mc-stop "$SERVER_DIR" --confirm >/dev/null 2>&1 || true
wait_for_server_stop "$SERVER_DIR" 20 >/dev/null 2>&1 || true
ln -sfn "$RUN_DIR" "$LATEST_LINK"
write_summary
@ -286,7 +288,7 @@ if [[ ! -d "$MCC_SERVERS/$SERVER_DIR" ]]; then
fail "Server directory not found: $MCC_SERVERS/$SERVER_DIR"
fi
bash "$SCRIPT_DIR/prepare_offline_mcc_config.sh" "$CFG" "$MC_VERSION" CursorBot >/dev/null || fail "Failed to prepare temporary MCC config."
bash "$SCRIPT_DIR/prepare_offline_mcc_config.sh" "$CFG" "$MC_VERSION" "$TEST_USERNAME" >/dev/null || fail "Failed to prepare temporary MCC config."
PORT="$(bash "$SCRIPT_DIR/get_server_port.sh" "$SERVER_DIR")"
"$SCRIPT_DIR/ensure_offline_server.sh" "$SERVER_DIR"
@ -296,6 +298,7 @@ if [[ "$PROFILE" == "legacy" && -f "$MCC_SERVERS/$SERVER_DIR/server.properties"
sed_in_place 's/^use-native-transport=.*/use-native-transport=false/' "$MCC_SERVERS/$SERVER_DIR/server.properties"
fi
mkdir -p "$(dirname "$INPUT_FILE")"
: > "$INPUT_FILE"
rm -f "$MCC_LOG"
@ -306,9 +309,9 @@ wait_for_server_ready "$SERVER_DIR" || 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 -- \
MCC_FILE_INPUT=1 MCC_INPUT_FILE="$INPUT_FILE" dotnet run --project MinecraftClient -c Release --no-build -- \
"$CFG" \
CursorBot \
"$TEST_USERNAME" \
- \
"localhost:$PORT" \
"--accounttype=mojang" \
@ -323,9 +326,9 @@ log_step "Starting MCC for $MC_VERSION"
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."
wait_for_file_pattern "$SERVER_LOG_FILE" "$TEST_USERNAME joined the game" "server join entry" 30 || fail "Server never logged the join."
run_server_command "op CursorBot"
run_server_command "op $TEST_USERNAME"
run_server_command "gamerule sendCommandFeedback true"
if [[ "$PROFILE" == "modern" ]]; then
run_server_command "gamerule logAdminCommands true"

View file

@ -17,31 +17,31 @@ RUN_ROOT="${TMPDIR:-/tmp}/mcc-integration-testing"
RUN_ID="$(date +%Y%m%d-%H%M%S)"
RUN_DIR="$RUN_ROOT/$RUN_ID"
SERVER_LOG_FILE="$MCC_SERVERS/$VERSION/logs/latest.log"
MCC_LOG="$RUN_DIR/mcc.log"
SESSION_NAME="full-spectrum-${MC_VERSION//[^a-zA-Z0-9]/_}"
TEST_USERNAME="$(_mcc_resolve_username "$SESSION_NAME")"
MCC_LOG="$(_mcc_session_log_file "$SESSION_NAME")"
PID_FILE="$(_mcc_session_pid_file "$SESSION_NAME")"
MCC_TMUX_SESSION="$(_mcc_tmux_session_name "$SESSION_NAME")"
BUILD_LOG="$RUN_DIR/build.log"
SERVER_TMUX_LOG="$RUN_DIR/server-tmux.log"
SERVER_FILE_LOG="$RUN_DIR/server-latest.log"
INPUT_FILE="$REPO_ROOT/mcc_input.txt"
INPUT_FILE="$(_mcc_session_input_file "$SESSION_NAME")"
CFG="$RUN_DIR/MinecraftClient.$MC_VERSION.ini"
MCC_PID=""
mkdir -p "$RUN_DIR"
cleanup() {
if [[ -n "${MCC_PID:-}" ]] && kill -0 "$MCC_PID" 2>/dev/null; then
mcc-cmd "quit" >/dev/null 2>&1 || true
sleep 2
kill "$MCC_PID" 2>/dev/null || true
wait "$MCC_PID" 2>/dev/null || true
fi
mcc-cmd --session "$SESSION_NAME" "quit" >/dev/null 2>&1 || true
sleep 2
mcc-kill --session "$SESSION_NAME" >/dev/null 2>&1 || true
mc-stop "$VERSION" >/dev/null 2>&1 || true
mc-stop "$VERSION" --confirm >/dev/null 2>&1 || true
wait_for_server_stop "$VERSION" 20 >/dev/null 2>&1 || true
}
trap cleanup EXIT
prepare_config() {
bash "$SCRIPT_DIR/prepare_offline_mcc_config.sh" "$CFG" "$MC_VERSION" CursorBot >/dev/null
bash "$SCRIPT_DIR/prepare_offline_mcc_config.sh" "$CFG" "$MC_VERSION" "$TEST_USERNAME" >/dev/null
}
wait_for_server_log_pattern() {
@ -129,35 +129,57 @@ run_server_command() {
run_mcc_command() {
local cmd="$1"
echo "MCC> $cmd"
mcc-cmd "$cmd"
mcc-cmd --session "$SESSION_NAME" "$cmd"
sleep 2
}
start_mcc_session() {
local -a mcc_args=("$CFG" "$TEST_USERNAME" "-" "localhost:$SERVER_PORT")
local mcc_args_cmd
mcc_args_cmd="$(printf '%q ' "${mcc_args[@]}")"
tmux kill-session -t "$MCC_TMUX_SESSION" 2>/dev/null || true
rm -f "$PID_FILE"
tmux new-session -d -s "$MCC_TMUX_SESSION" -x 160 -y 50 \
"cd '$REPO_ROOT' && printf '%s\n' \"\$\$\" > '$PID_FILE' && exec env MCC_FILE_INPUT=1 MCC_INPUT_FILE='$INPUT_FILE' dotnet run --project MinecraftClient -c Release --no-build -- $mcc_args_cmd > '$MCC_LOG' 2>&1"
for _ in $(seq 1 25); do
if [[ -s "$PID_FILE" ]]; then
return 0
fi
sleep 0.2
done
fail "Failed to capture MCC PID for session $SESSION_NAME"
}
bash "$SCRIPT_DIR/preflight_test_env.sh" "$VERSION" >/dev/null
bash "$SCRIPT_DIR/reset_shared_test_state.sh" "$VERSION" >/dev/null
"$SCRIPT_DIR/ensure_offline_server.sh" "$VERSION"
mcc-reset-session --session "$SESSION_NAME" >/dev/null
echo "Building MCC..."
mcc-build > "$BUILD_LOG" 2>&1 || fail "mcc-build failed"
prepare_config
SERVER_PORT="$(bash "$SCRIPT_DIR/get_server_port.sh" "$VERSION")"
if [[ -z "$SERVER_PORT" ]]; then
fail "Failed to resolve server port"
fi
mkdir -p "$(dirname "$INPUT_FILE")" "$(dirname "$MCC_LOG")"
: > "$INPUT_FILE"
rm -f "$MCC_LOG"
echo "Starting server..."
mc-start "$VERSION" >/dev/null
wait_for_server_ready "$VERSION" || fail "Server did not become ready"
echo "Starting MCC..."
(
cd "$REPO_ROOT"
MCC_FILE_INPUT=1 dotnet run --project MinecraftClient -c Release --no-build -- "$CFG" CursorBot - "localhost:$SERVER_PORT" > "$MCC_LOG" 2>&1
) &
MCC_PID=$!
start_mcc_session
wait_for_file_pattern "$MCC_LOG" "Server was successfully joined." "MCC join success" 90 || fail "MCC failed to join"
wait_for_server_log_pattern "CursorBot joined the game" "server join entry" 30 || fail "Server never logged the join"
wait_for_server_log_pattern "$TEST_USERNAME joined the game" "server join entry" 30 || fail "Server never logged the join"
run_server_command "op CursorBot"
run_server_command "op $TEST_USERNAME"
run_server_command "gamerule sendCommandFeedback true"
run_server_command "gamerule logAdminCommands true"
run_server_command "time set day"
@ -176,7 +198,7 @@ run_mcc_command "/time query daytime"
run_mcc_command "smoke_test_from_mcc_full_spectrum"
# ── Phase 2: Movement and look commands ──
run_mcc_command "/tp CursorBot 0 -60 0"
run_mcc_command "/tp $TEST_USERNAME 0 -60 0"
sleep 3
run_mcc_command "look up"
sleep 1
@ -193,35 +215,35 @@ run_mcc_command "inventory creativeclear 38"
run_mcc_command "inventory player list"
# ── Phase 4: Block placement and interaction ──
run_server_command "execute as CursorBot at @s run fill ~1 ~ ~1 ~3 ~2 ~3 minecraft:stone"
run_server_command "execute as $TEST_USERNAME at @s run fill ~1 ~ ~1 ~3 ~2 ~3 minecraft:stone"
sleep 2
run_server_command "execute as CursorBot at @s run setblock ~5 ~ ~5 minecraft:chest"
run_server_command "execute as $TEST_USERNAME at @s run setblock ~5 ~ ~5 minecraft:chest"
sleep 1
run_server_command "execute as CursorBot at @s run setblock ~5 ~1 ~5 minecraft:furnace"
run_server_command "execute as $TEST_USERNAME at @s run setblock ~5 ~1 ~5 minecraft:furnace"
sleep 1
run_server_command "execute as CursorBot at @s run setblock ~6 ~ ~5 minecraft:crafting_table"
run_server_command "execute as $TEST_USERNAME at @s run setblock ~6 ~ ~5 minecraft:crafting_table"
sleep 1
# ── Phase 5: Entity spawning (expanded coverage) ──
run_server_command "execute as CursorBot at @s run summon minecraft:cow ~2 ~ ~"
run_server_command "execute as CursorBot at @s run summon minecraft:zombie ~4 ~ ~"
run_server_command "execute as CursorBot at @s run summon minecraft:creeper ~6 ~ ~"
run_server_command "execute as CursorBot at @s run summon minecraft:skeleton ~8 ~ ~"
run_server_command "execute as CursorBot at @s run summon minecraft:villager ~-2 ~ ~"
run_server_command "execute as CursorBot at @s run summon minecraft:allay ~-4 ~ ~"
run_server_command "execute as CursorBot at @s run summon minecraft:armor_stand ~ ~ ~2"
run_server_command "execute as CursorBot at @s run summon minecraft:item_display ~-6 ~ ~ {item:{id:\"minecraft:diamond\",count:1}}"
run_server_command "execute as CursorBot at @s run summon minecraft:spider ~10 ~ ~"
run_server_command "execute as CursorBot at @s run summon minecraft:pig ~-8 ~ ~"
run_server_command "execute as $TEST_USERNAME at @s run summon minecraft:cow ~2 ~ ~"
run_server_command "execute as $TEST_USERNAME at @s run summon minecraft:zombie ~4 ~ ~"
run_server_command "execute as $TEST_USERNAME at @s run summon minecraft:creeper ~6 ~ ~"
run_server_command "execute as $TEST_USERNAME at @s run summon minecraft:skeleton ~8 ~ ~"
run_server_command "execute as $TEST_USERNAME at @s run summon minecraft:villager ~-2 ~ ~"
run_server_command "execute as $TEST_USERNAME at @s run summon minecraft:allay ~-4 ~ ~"
run_server_command "execute as $TEST_USERNAME at @s run summon minecraft:armor_stand ~ ~ ~2"
run_server_command "execute as $TEST_USERNAME at @s run summon minecraft:item_display ~-6 ~ ~ {item:{id:\"minecraft:diamond\",count:1}}"
run_server_command "execute as $TEST_USERNAME at @s run summon minecraft:spider ~10 ~ ~"
run_server_command "execute as $TEST_USERNAME at @s run summon minecraft:pig ~-8 ~ ~"
sleep 2
run_mcc_command "entity"
# ── Phase 6: Effects and environment ──
run_server_command "effect give CursorBot minecraft:speed 30 1"
run_server_command "effect give $TEST_USERNAME minecraft:speed 30 1"
sleep 2
run_mcc_command "health"
run_server_command "effect give CursorBot minecraft:regeneration 10 1"
run_server_command "effect give $TEST_USERNAME minecraft:regeneration 10 1"
sleep 2
run_mcc_command "health"
@ -233,39 +255,39 @@ run_mcc_command "/gamemode creative"
sleep 2
# ── Phase 8: Dimension change (nether) ──
run_server_command "execute in minecraft:the_nether run tp CursorBot 0 64 0"
run_server_command "execute in minecraft:the_nether run tp $TEST_USERNAME 0 64 0"
sleep 4
run_mcc_command "health"
run_server_command "execute in minecraft:overworld run tp CursorBot 0 -60 0"
run_server_command "execute in minecraft:overworld run tp $TEST_USERNAME 0 -60 0"
sleep 4
# ── Phase 9: Server chat and whisper ──
run_server_command "say Hello from the server console"
sleep 2
run_server_command "msg CursorBot This is a private whisper"
run_server_command "msg $TEST_USERNAME This is a private whisper"
sleep 2
run_mcc_command "integration_test_chat_response"
# ── Phase 10: Particles, sounds, and explosions ──
run_server_command "execute as CursorBot at @s run particle minecraft:happy_villager ~ ~1 ~ 0.5 0.5 0.5 0 12 force"
run_server_command "execute as CursorBot at @s run particle minecraft:end_rod ~ ~1 ~ 0.5 0.5 0.5 0.01 20 force"
run_server_command "execute as CursorBot at @s run particle minecraft:explosion ~ ~1 ~ 0 0 0 0 1 force"
run_server_command "execute as CursorBot at @s run particle minecraft:totem_of_undying ~ ~1 ~ 0.5 0.5 0.5 0.1 20 force"
run_server_command "execute as CursorBot at @s run particle minecraft:flame ~ ~1 ~ 0.2 0.2 0.2 0.02 30 force"
run_server_command "execute as CursorBot at @s run particle minecraft:heart ~ ~2 ~ 0.3 0.3 0.3 0 5 force"
run_server_command "execute as $TEST_USERNAME at @s run particle minecraft:happy_villager ~ ~1 ~ 0.5 0.5 0.5 0 12 force"
run_server_command "execute as $TEST_USERNAME at @s run particle minecraft:end_rod ~ ~1 ~ 0.5 0.5 0.5 0.01 20 force"
run_server_command "execute as $TEST_USERNAME at @s run particle minecraft:explosion ~ ~1 ~ 0 0 0 0 1 force"
run_server_command "execute as $TEST_USERNAME at @s run particle minecraft:totem_of_undying ~ ~1 ~ 0.5 0.5 0.5 0.1 20 force"
run_server_command "execute as $TEST_USERNAME at @s run particle minecraft:flame ~ ~1 ~ 0.2 0.2 0.2 0.02 30 force"
run_server_command "execute as $TEST_USERNAME at @s run particle minecraft:heart ~ ~2 ~ 0.3 0.3 0.3 0 5 force"
run_server_command "execute as CursorBot at @s run playsound minecraft:entity.lightning_bolt.thunder master CursorBot ~ ~ ~ 1 1 0"
run_server_command "execute as CursorBot at @s run playsound minecraft:block.note_block.bell master CursorBot ~ ~ ~ 1 1 0"
run_server_command "execute as CursorBot at @s run playsound minecraft:entity.experience_orb.pickup master CursorBot ~ ~ ~ 1 1 0"
run_server_command "execute as $TEST_USERNAME at @s run playsound minecraft:entity.lightning_bolt.thunder master $TEST_USERNAME ~ ~ ~ 1 1 0"
run_server_command "execute as $TEST_USERNAME at @s run playsound minecraft:block.note_block.bell master $TEST_USERNAME ~ ~ ~ 1 1 0"
run_server_command "execute as $TEST_USERNAME at @s run playsound minecraft:entity.experience_orb.pickup master $TEST_USERNAME ~ ~ ~ 1 1 0"
run_server_command "execute as CursorBot at @s run summon minecraft:tnt ~3 ~ ~"
run_server_command "execute as $TEST_USERNAME at @s run summon minecraft:tnt ~3 ~ ~"
sleep 2
run_server_command "execute as CursorBot at @s run summon minecraft:tnt ~6 ~ ~"
run_server_command "execute as $TEST_USERNAME at @s run summon minecraft:tnt ~6 ~ ~"
# ── Phase 11: Kill and respawn cycle ──
run_mcc_command "/gamemode survival"
sleep 2
run_server_command "kill CursorBot"
run_server_command "kill $TEST_USERNAME"
sleep 4
run_mcc_command "respawn"
sleep 4
@ -295,14 +317,14 @@ assert_not_contains "$MCC_LOG" "Failed to load settings" "MCC failed to reload i
assert_not_contains "$MCC_LOG" "NullReferenceException" "A NullReferenceException occurred during the test"
# ── Assertions: Server log ──
assert_contains "$SERVER_FILE_LOG" "CursorBot joined the game" "Server never saw CursorBot join"
assert_contains "$SERVER_FILE_LOG" "$TEST_USERNAME joined the game" "Server never saw $TEST_USERNAME join"
assert_contains "$SERVER_FILE_LOG" "smoke_test_from_mcc_full_spectrum" "Server never received the client chat message"
assert_contains "$SERVER_FILE_LOG" "Displaying particle minecraft:happy_villager" "Particle events were not recorded on the server"
assert_contains "$SERVER_FILE_LOG" "Played sound minecraft:block.note_block.bell to CursorBot" "Sound events were not recorded on the server"
assert_contains "$SERVER_FILE_LOG" "Played sound minecraft:block.note_block.bell to $TEST_USERNAME" "Sound events were not recorded on the server"
assert_contains "$SERVER_FILE_LOG" "Summoned new Primed TNT" "TNT summon did not occur on the server"
assert_contains "$SERVER_FILE_LOG" "integration_test_chat_response" "Server never received the chat response test message"
assert_contains "$SERVER_FILE_LOG" "Hello from the server console" "Server say command was not logged"
assert_contains "$SERVER_FILE_LOG" "Killed CursorBot" "Server kill command did not execute"
assert_contains "$SERVER_FILE_LOG" "Killed $TEST_USERNAME" "Server kill command did not execute"
assert_not_contains "$SERVER_FILE_LOG" "Sending unknown packet 'clientbound/minecraft:disconnect'" "Server hit the disconnect packet regression during the test"
cat <<EOF

View file

@ -0,0 +1,199 @@
#!/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"
# shellcheck source=.skills/mcc-integration-testing/scripts/common.sh
source "$SCRIPT_DIR/common.sh"
VERSION="${1:-1.21.11-Vanilla}"
MC_VERSION="${VERSION%-Vanilla}"
if [[ "$MC_VERSION" == "$VERSION" ]]; then
MC_VERSION="$VERSION"
fi
SESSION_A="parallel-smoke-a-${MC_VERSION//[^a-zA-Z0-9]/_}"
SESSION_B="parallel-smoke-b-${MC_VERSION//[^a-zA-Z0-9]/_}"
USERNAME_A="SmokeA"
USERNAME_B="SmokeB"
RUN_ROOT="${TMPDIR:-/tmp}/mcc-integration-testing"
RUN_ID="$(date +%Y%m%d-%H%M%S)"
RUN_DIR="$RUN_ROOT/parallel-smoke-$RUN_ID"
BUILD_LOG="$RUN_DIR/build.log"
SERVER_TMUX_LOG="$RUN_DIR/server-tmux.log"
SERVER_FILE_LOG="$RUN_DIR/server-latest.log"
SERVER_LOG_FILE="$MCC_SERVERS/$VERSION/logs/latest.log"
LOG_A="$(_mcc_session_log_file "$SESSION_A")"
LOG_B="$(_mcc_session_log_file "$SESSION_B")"
INPUT_A="$(_mcc_session_input_file "$SESSION_A")"
INPUT_B="$(_mcc_session_input_file "$SESSION_B")"
PID_A_FILE="$(_mcc_session_pid_file "$SESSION_A")"
PID_B_FILE="$(_mcc_session_pid_file "$SESSION_B")"
mkdir -p "$RUN_DIR"
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_log_pattern() {
local pattern="$1"
local description="$2"
local timeout="${3:-60}"
local elapsed=0
while (( elapsed < timeout )); do
if [[ -f "$SERVER_LOG_FILE" ]] && grep -Fq "$pattern" "$SERVER_LOG_FILE"; then
return 0
fi
sleep 1
((elapsed += 1))
done
echo "Timed out waiting for server log: $description" >&2
return 1
}
capture_server_logs() {
mc-log "$VERSION" 400 > "$SERVER_TMUX_LOG" 2>/dev/null || true
if [[ -f "$SERVER_LOG_FILE" ]]; then
cp "$SERVER_LOG_FILE" "$SERVER_FILE_LOG"
fi
}
fail() {
capture_server_logs
echo "FAIL: $1" >&2
echo "Run directory: $RUN_DIR" >&2
exit 1
}
cleanup() {
mcc-cmd --session "$SESSION_A" "quit" >/dev/null 2>&1 || true
mcc-cmd --session "$SESSION_B" "quit" >/dev/null 2>&1 || true
sleep 1
mcc-kill --session "$SESSION_A" >/dev/null 2>&1 || true
mcc-kill --session "$SESSION_B" >/dev/null 2>&1 || true
mc-stop "$VERSION" --confirm >/dev/null 2>&1 || true
wait_for_server_stop "$VERSION" 20 >/dev/null 2>&1 || true
}
trap cleanup EXIT
assert_session_alive() {
local session="$1"
local pid_file="$2"
if [[ -s "$pid_file" ]]; then
local pid
pid="$(tr -cd '0-9' < "$pid_file")"
if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
return 0
fi
fi
tmux has-session -t "$(_mcc_tmux_session_name "$session")" 2>/dev/null
}
assert_server_alive() {
server_running "$VERSION" || fail "Shared server session is not alive"
}
start_file_input_session() {
local session="$1"
local username="$2"
local port="$3"
bash "$REPO_ROOT/tools/mcc-debug.sh" \
--version "$VERSION" \
--port "$port" \
--file-input \
--no-build \
--session "$session" \
--username "$username" >/dev/null
}
bash "$SCRIPT_DIR/preflight_test_env.sh" "$VERSION" >/dev/null
bash "$SCRIPT_DIR/reset_shared_test_state.sh" "$VERSION" >/dev/null
"$SCRIPT_DIR/ensure_offline_server.sh" "$VERSION" >/dev/null
mcc-reset-session --session "$SESSION_A" >/dev/null
mcc-reset-session --session "$SESSION_B" >/dev/null
echo "Building MCC..."
mcc-build > "$BUILD_LOG" 2>&1 || fail "mcc-build failed"
echo "Starting shared server..."
mc-start "$VERSION" >/dev/null
wait_for_server_ready "$VERSION" || fail "Server did not become ready"
SERVER_PORT="$(bash "$SCRIPT_DIR/get_server_port.sh" "$VERSION")"
if [[ -z "$SERVER_PORT" ]]; then
fail "Failed to resolve server port"
fi
echo "Starting MCC session A..."
start_file_input_session "$SESSION_A" "$USERNAME_A" "$SERVER_PORT"
echo "Starting MCC session B..."
start_file_input_session "$SESSION_B" "$USERNAME_B" "$SERVER_PORT"
wait_for_file_pattern "$LOG_A" "Server was successfully joined." "session A join success" 90 || fail "Session A failed to join"
wait_for_file_pattern "$LOG_B" "Server was successfully joined." "session B join success" 90 || fail "Session B failed to join"
wait_for_server_log_pattern "$USERNAME_A joined the game" "server join for session A" 30 || fail "Server never logged $USERNAME_A join"
wait_for_server_log_pattern "$USERNAME_B joined the game" "server join for session B" 30 || fail "Server never logged $USERNAME_B join"
echo "Sending debug state to both sessions..."
mcc-cmd --session "$SESSION_A" "debug state"
mcc-cmd --session "$SESSION_B" "debug state"
sleep 2
wait_for_file_pattern "$LOG_A" "[FileInput] > debug state" "session A debug state command" 20 || fail "Session A did not consume debug state"
wait_for_file_pattern "$LOG_B" "[FileInput] > debug state" "session B debug state command" 20 || fail "Session B did not consume debug state"
echo "Killing session A..."
mcc-kill --session "$SESSION_A" >/dev/null 2>&1 || true
sleep 2
assert_session_alive "$SESSION_B" "$PID_B_FILE" || fail "Session B is not alive after killing session A"
assert_server_alive
echo "Verifying session B still responds..."
mcc-cmd --session "$SESSION_B" "health"
wait_for_file_pattern "$LOG_B" "[FileInput] > health" "session B health command after session A kill" 20 || fail "Session B stopped responding after session A kill"
if [[ -s "$PID_A_FILE" ]]; then
pid_a="$(tr -cd '0-9' < "$PID_A_FILE")"
if [[ -n "$pid_a" ]] && kill -0 "$pid_a" 2>/dev/null; then
fail "Session A is still alive after mcc-kill"
fi
fi
capture_server_logs
cat <<EOF
PASS
Run directory: $RUN_DIR
Server version: $VERSION
Server port: $SERVER_PORT
Session A: $SESSION_A ($USERNAME_A)
Session A input: $INPUT_A
Session A log: $LOG_A
Session B: $SESSION_B ($USERNAME_B)
Session B input: $INPUT_B
Session B log: $LOG_B
Server log: $SERVER_FILE_LOG
Build log: $BUILD_LOG
EOF

View file

@ -22,4 +22,4 @@ echo "MCC highlights:"
grep -E "Server was successfully joined|FileInput|smoke_test_from_mcc_full_spectrum|There are [0-9]+ of a max|health|Creative" "$MCC_LOG" || true
echo
echo "Server highlights:"
grep -E "joined the game|Made CursorBot a server operator|game mode|smoke_test_from_mcc_full_spectrum|summon|particle|playsound|tnt" "$SERVER_LOG" || true
grep -E "joined the game|Made .* a server operator|game mode|smoke_test_from_mcc_full_spectrum|summon|particle|playsound|tnt" "$SERVER_LOG" || true

View file

@ -30,7 +30,7 @@ try
ToolEnvelope initialWorldState = await CallSuccessAsync(client, executed, "mcc_world_state");
JsonElement initialWorldData = RequireData(initialWorldState);
string botName = ReadString(initialWorldData, "username") ?? "CursorBot";
string botName = ReadString(initialWorldData, "username") ?? "MCCBot";
Coordinate initialLocation = ReadCoordinate(initialWorldData, "location");
long setupBaseline = 0;

View file

@ -8,7 +8,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ModelContextProtocol" Version="1.1.0" />
<PackageReference Include="ModelContextProtocol" Version="1.2.0" />
</ItemGroup>
<ItemGroup>

View file

@ -8,7 +8,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ModelContextProtocol" Version="1.1.0" />
<PackageReference Include="ModelContextProtocol" Version="1.2.0" />
</ItemGroup>
</Project>

11
Directory.Build.props Normal file
View file

@ -0,0 +1,11 @@
<Project>
<PropertyGroup>
<DefaultItemExcludes>$(DefaultItemExcludes);**/bin/**;**/obj/**</DefaultItemExcludes>
</PropertyGroup>
<PropertyGroup Condition="'$(MCC_BUILD_ROOT)' != ''">
<BaseOutputPath>$(MCC_BUILD_ROOT)/$(MSBuildProjectName)/bin/</BaseOutputPath>
<BaseIntermediateOutputPath>$(MCC_BUILD_ROOT)/$(MSBuildProjectName)/obj/</BaseIntermediateOutputPath>
<MSBuildProjectExtensionsPath>$(BaseIntermediateOutputPath)</MSBuildProjectExtensionsPath>
</PropertyGroup>
</Project>

View file

@ -38,20 +38,20 @@
<ItemGroup>
<PackageReference Include="Brigadier.NET" Version="1.2.13" />
<PackageReference Include="DiscordRichPresence" Version="1.143.0" />
<PackageReference Include="Consolonia" Version="11.3.10" />
<PackageReference Include="Consolonia" Version="11.3.12.3" />
<PackageReference Include="DnsClient" Version="1.8.0" />
<PackageReference Include="DSharpPlus" Version="4.5.1" />
<PackageReference Include="DynamicExpresso.Core" Version="2.19.3" />
<PackageReference Include="FuzzySharp" Version="2.0.2" />
<PackageReference Include="Magick.NET-Q16-AnyCPU" Version="14.11.1" />
<PackageReference Include="MessagePack" Version="3.1.4" />
<PackageReference Include="ModelContextProtocol" Version="1.1.0" />
<PackageReference Include="ModelContextProtocol.AspNetCore" Version="1.1.0" />
<PackageReference Include="ModelContextProtocol" Version="1.2.0" />
<PackageReference Include="ModelContextProtocol.AspNetCore" Version="1.2.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="5.3.0" />
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="6.0.0" />
<PackageReference Include="Microsoft.Windows.Compatibility" Version="10.0.5" />
<PackageReference Include="Samboy063.Tomlet" Version="6.2.0" />
<PackageReference Include="Sentry" Version="6.3.0" />
<PackageReference Include="Sentry" Version="6.3.1" />
<PackageReference Include="SingleFileExtractor.Core" Version="2.3.0" />
<PackageReference Include="starksoft.aspen" Version="1.1.8">
<NoWarn>NU1701</NoWarn>

View file

@ -367,10 +367,16 @@ This gives you the helper functions used by the workflow:
- `mc-cmd`
- `mc-log`
- `mc-rcon`
- `mc-reset-test-env`
- `mcc-build`
- `mcc-build-clean`
- `mcc-run`
- `mcc-tui`
- `mcc-cmd`
- `mcc-log-mcc`
- `mcc-state`
- `mcc-kill`
- `mcc-debug`
- `mcc-reload`
</details>
@ -412,7 +418,7 @@ Then make sure the helper functions are loaded:
```bash
type mc-start
type mcc-build
type mcc-run
type mcc-debug
```
</details>
@ -426,11 +432,55 @@ The moving parts are:
- a local Minecraft server running in `tmux`
- `mc-rcon` for server-side commands such as `/op`, `/give`, `/summon`, or gamerule setup
- MCC started with `MCC_FILE_INPUT=1`
- `FileInputBot`, which watches `mcc_input.txt` and turns file lines into MCC commands or server chat
- `FileInputBot`, which watches the session input file under `${TMPDIR:-/tmp}/mcc-debug/<session>/mcc_input.txt`
- logs from MCC and the local server, which the agent can inspect between runs
The result is simple: the agent can change code, rebuild, start the app, inject commands, and read the result without waiting for a human to sit in the terminal.
## Shared Server, Isolated MCC Sessions
The harness separates shared server state from MCC client state.
- `mc-*` commands operate on the shared local Minecraft server.
- `mcc-*` commands operate on one MCC client session.
- The default `session` is the current worktree name.
- The default username is derived from `session`, so two worktrees can join the same shared server without kicking each other.
- Session files live under `${TMPDIR:-/tmp}/mcc-debug/<session>/`.
- `MCC_SERVERS` stays the shared server-root override.
Keep shared servers running by default. Do not stop or reset them unless the user explicitly asks for that, or you need to switch server versions.
Two worktrees can share one local server like this:
```bash
# worktree A
cd ~/Minecraft/Minecraft-Console-Client
source tools/mcc-env.sh
mc-start 1.21.11
mcc-debug -v 1.21.11 --file-input
# worktree B
cd ~/Minecraft/Minecraft-Console-Client-foo
source tools/mcc-env.sh
mcc-debug -v 1.21.11 --file-input
# from each worktree, mcc-* targets that worktree's default session
mcc-state
```
If you need two MCC sessions from one worktree, pass `--session NAME` explicitly.
### tmpfs build mode
```bash
source tools/mcc-env.sh
export MCC_BUILD_MODE=tmpfs
mcc-build
mcc-build-clean
```
When `MCC_BUILD_MODE=tmpfs`, build output goes to `/dev/shm/mcc-build/<worktree>/` on Linux, or `${TMPDIR:-/tmp}/mcc-build/<worktree>/` when `/dev/shm` is unavailable.
## Repository Tools
These are the repo-level tools that make the workflow practical.
@ -438,6 +488,7 @@ These are the repo-level tools that make the workflow practical.
| Path | Purpose |
| ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `tools/mcc-env.sh` | Loads the shell helper functions used for the normal loop. |
| `tools/mcc-debug.sh` | Starts a session-scoped MCC debug run with generated config, log, pid, and tmux state. |
| `tools/start-server.sh` | Starts a local Minecraft server in a named `tmux` session with a FIFO for stdin. |
| `tools/mc-rcon.sh` | Sends RCON commands to the local server using `python3`. |
| `tools/decompile.sh` | Downloads `MinecraftDecompiler.jar` if needed, decompiles the requested Minecraft version, and fetches `server.jar` for server-side work. |
@ -453,7 +504,7 @@ There is one more piece worth calling out:
- `MinecraftClient/ChatBots/FileInputBot.cs` is what makes file-driven command injection possible.
- It is loaded when `MCC_FILE_INPUT=1` is set.
- `mcc-run` in `tools/mcc-env.sh` already sets that flag for you.
- `mcc-debug --file-input` and `mcc-run` already set that flag for you.
## Skills
@ -485,9 +536,12 @@ Some skills also carry their own bundled resources:
This is the core loop you should expect an agent to follow.
### 1. Start the local server
### 1. Start the local server and resolve the MCC identity
```bash
source tools/mcc-env.sh
SESSION="smoke-a"
USERNAME="$(_mcc_resolve_username "$SESSION")"
mc-start 1.20.6
```
@ -506,13 +560,7 @@ mcc-build
### 3. Run MCC with file input enabled
```bash
mcc-run
```
The raw form looks like this:
```bash
MCC_FILE_INPUT=1 dotnet run --project MinecraftClient -c Release -- CursorBot - localhost:25565
mcc-debug -v 1.20.6 --file-input --session "$SESSION" --no-build
```
### 4. Set up server state through RCON
@ -520,20 +568,20 @@ MCC_FILE_INPUT=1 dotnet run --project MinecraftClient -c Release -- CursorBot -
Examples:
```bash
mc-rcon "op CursorBot"
mc-rcon "op $USERNAME"
mc-rcon "gamerule sendCommandFeedback true"
mc-rcon "give CursorBot diamond_sword 1"
mc-rcon "give $USERNAME diamond_sword 1"
mc-rcon "summon minecraft:armor_stand ~ ~ ~"
```
### 5. Drive MCC through `mcc_input.txt`
### 5. Drive MCC through the session input file
Examples:
```bash
mcc-cmd "inventory player list"
mcc-cmd "entity"
mcc-cmd "/gamemode creative"
mcc-cmd --session "$SESSION" "inventory player list"
mcc-cmd --session "$SESSION" "entity"
mcc-cmd --session "$SESSION" "/gamemode creative"
```
Behavior:
@ -638,12 +686,15 @@ Use skills:
Typical loop:
```bash
source tools/mcc-env.sh
SESSION="smoke-a"
USERNAME="$(_mcc_resolve_username "$SESSION")"
mc-start 1.20.6
mcc-build
mcc-run
mc-rcon "op CursorBot"
mcc-cmd "inventory player list"
mcc-cmd "entity"
mcc-debug -v 1.20.6 --file-input --session "$SESSION" --no-build
mc-rcon "op $USERNAME"
mcc-cmd --session "$SESSION" "inventory player list"
mcc-cmd --session "$SESSION" "entity"
```
Then inspect the MCC output, patch the code, and use:

View file

@ -0,0 +1,789 @@
# MCC 共享服务器与隔离会话 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 让多个 Git worktree 共享同一套本地 Minecraft 测试服务器,同时让每个 MCC 调试会话按 `session` 隔离,并支持按 worktree 选择性把构建输出放到 tmpfs。
**Architecture:** 保持 `mc-*` 命令面向共享服务器,保留 `MCC_SERVERS` 作为共享 server root override`mcc-*` 命令改成面向显式 `session` 的客户端会话工具,默认 `session` 取当前 worktree 名,默认用户名从 `session` 派生。构建层通过统一的 helper 和 `Directory.Build.props` 接入可选的 tmpfs 构建根,不再依赖 shell 中泄漏的 `MCC_REPO`
**Tech Stack:** Bash, tmux, dotnet CLI, MSBuild `Directory.Build.props`, repo 自带的 MCC integration harness
---
### Task 1: 建立会话与构建根解析的基础 helper并补一个可重复运行的 shell smoke test
**Files:**
- Create: `tools/test-mcc-env.sh`
- Modify: `tools/mcc-env.sh`
- [ ] **Step 1: 先写一个会失败的 shell smoke test锁定会话名、用户名和路径解析规则**
```bash
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
source "$REPO_ROOT/tools/mcc-env.sh"
assert_eq() {
local expected="$1"
local actual="$2"
local label="$3"
if [[ "$expected" != "$actual" ]]; then
echo "FAIL: $label" >&2
echo " expected: $expected" >&2
echo " actual: $actual" >&2
exit 1
fi
}
assert_regex() {
local regex="$1"
local actual="$2"
local label="$3"
if [[ ! "$actual" =~ $regex ]]; then
echo "FAIL: $label" >&2
echo " regex: $regex" >&2
echo " actual: $actual" >&2
exit 1
fi
}
session="$(_mcc_resolve_session "demo-branch")"
assert_eq "demo-branch" "$session" "explicit session"
short_name="$(_mcc_resolve_username "feature-ai")"
assert_eq "mcc_feature_ai" "$short_name" "short derived username"
long_name="$(_mcc_resolve_username "very-long-worktree-name")"
assert_regex '^mcc_[a-z0-9_]{7}_[0-9a-f]{4}$' "$long_name" "long derived username shape"
assert_eq "16" "${#long_name}" "long derived username length"
assert_eq "${TMPDIR:-/tmp}/mcc-debug/demo-branch" "$(_mcc_session_root "demo-branch")" "session root"
assert_eq "mcc-demo-branch" "$(_mcc_tmux_session_name "demo-branch")" "tmux session name"
MCC_BUILD_MODE=tmpfs
build_root="$(_mcc_build_root)"
assert_regex '^(/dev/shm|/tmp)/mcc-build/.+$' "$build_root" "tmpfs build root"
echo "PASS"
```
- [ ] **Step 2: 运行测试,确认它先失败**
Run: `bash tools/test-mcc-env.sh`
Expected: FAIL错误类似 `_mcc_resolve_session: command not found`
- [ ] **Step 3: 在 `tools/mcc-env.sh` 中加入 repo root、server root、session、username、会话目录、tmux 名和 build root helper**
```bash
if [[ -n "${BASH_SOURCE[0]:-}" ]]; then
_mcc_env_source="${BASH_SOURCE[0]}"
elif [[ -n "${ZSH_VERSION:-}" ]]; then
_mcc_env_source="${(%):-%N}"
else
_mcc_env_source="$0"
fi
TOOLS_DIR="$(cd "$(dirname "$_mcc_env_source")" && pwd)"
MCC_REPO_ROOT="$(cd "$TOOLS_DIR/.." && pwd)"
unset _mcc_env_source
_mcc_repo_root() {
printf '%s\n' "$MCC_REPO_ROOT"
}
_mcc_servers_root() {
printf '%s\n' "${MCC_SERVERS:-$MCC_REPO_ROOT/MinecraftOfficial/downloads}"
}
_mcc_current_worktree_name() {
git -C "$MCC_REPO_ROOT" rev-parse --show-toplevel 2>/dev/null | xargs basename
}
_mcc_resolve_session() {
local explicit="${1:-}"
if [[ -n "$explicit" ]]; then
printf '%s\n' "$explicit"
return 0
fi
local worktree
worktree="$(_mcc_current_worktree_name)"
if [[ -n "$worktree" ]]; then
printf '%s\n' "$worktree"
else
basename "$MCC_REPO_ROOT"
fi
}
_mcc_sha1_short() {
if command -v sha1sum >/dev/null 2>&1; then
printf '%s' "$1" | sha1sum | awk '{print substr($1, 1, 4)}'
else
printf '%s' "$1" | shasum -a 1 | awk '{print substr($1, 1, 4)}'
fi
}
_mcc_resolve_username() {
local session="$1"
local normalized
normalized="$(printf '%s' "$session" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9_]/_/g')"
local candidate="mcc_${normalized}"
if (( ${#candidate} <= 16 )); then
printf '%s\n' "$candidate"
return 0
fi
local hash
hash="$(_mcc_sha1_short "$normalized")"
printf '%s_%s\n' "${candidate:0:11}" "$hash"
}
_mcc_session_root() {
printf '%s/mcc-debug/%s\n' "${TMPDIR:-/tmp}" "$1"
}
_mcc_session_log_file() {
printf '%s/mcc-debug.log\n' "$(_mcc_session_root "$1")"
}
_mcc_session_input_file() {
printf '%s/mcc_input.txt\n' "$(_mcc_session_root "$1")"
}
_mcc_session_pid_file() {
printf '%s/mcc.pid\n' "$(_mcc_session_root "$1")"
}
_mcc_session_meta_file() {
printf '%s/session.meta\n' "$(_mcc_session_root "$1")"
}
_mcc_tmux_session_name() {
printf 'mcc-%s\n' "$1"
}
_mcc_build_root() {
local worktree
worktree="$(_mcc_current_worktree_name)"
if [[ "${MCC_BUILD_MODE:-local}" == "tmpfs" ]]; then
if [[ -d /dev/shm && -w /dev/shm ]]; then
printf '/dev/shm/mcc-build/%s\n' "$worktree"
else
printf '%s/mcc-build/%s\n' "${TMPDIR:-/tmp}" "$worktree"
fi
return 0
fi
printf '%s\n' "$MCC_REPO_ROOT"
}
```
- [ ] **Step 4: 再跑 smoke test确认规则都落地**
Run: `bash tools/test-mcc-env.sh`
Expected: PASS
- [ ] **Step 5: 提交这一小步**
```bash
git add tools/mcc-env.sh tools/test-mcc-env.sh
git commit -m "tools: add MCC session and build root helpers"
```
### Task 2: 让 `mcc-*` shell helper 全部按 `session` 工作,并新增安全的 session reset
**Files:**
- Modify: `tools/mcc-env.sh`
- Test: `tools/test-mcc-env.sh`
- [ ] **Step 1: 在 smoke test 里加入 `mcc-cmd``mcc-reset-session` 的行为检查**
```bash
session="wrapper-smoke"
input_file="$(_mcc_session_input_file "$session")"
rm -rf "$(_mcc_session_root "$session")"
mcc-cmd --session "$session" "debug state"
grep -Fq "debug state" "$input_file"
mcc-reset-session --session "$session"
[[ ! -e "$(_mcc_session_root "$session")" ]]
```
- [ ] **Step 2: 运行测试,确认它先因为参数解析或命令不存在而失败**
Run: `bash tools/test-mcc-env.sh`
Expected: FAIL错误类似 `mcc-reset-session: command not found``Unknown option: --session`
- [ ] **Step 3: 在 `tools/mcc-env.sh` 里把 `mcc-*` helper 改成 session-aware**
```bash
mcc-build() {
local repo_root
repo_root="$(_mcc_repo_root)"
if [[ "${MCC_BUILD_MODE:-local}" == "tmpfs" ]]; then
local build_root
build_root="$(_mcc_build_root)"
mkdir -p "$build_root"
MCC_BUILD_ROOT="$build_root" dotnet build "$repo_root/MinecraftClient.sln" -c Release
else
dotnet build "$repo_root/MinecraftClient.sln" -c Release
fi
}
mcc-cmd() {
local session=""
local command=""
while [[ $# -gt 0 ]]; do
case "$1" in
--session) session="$2"; shift 2 ;;
*) command="$1"; shift ;;
esac
done
[[ -n "$command" ]] || { echo "Usage: mcc-cmd [--session NAME] <command>" >&2; return 1; }
session="$(_mcc_resolve_session "$session")"
local input_file
input_file="$(_mcc_session_input_file "$session")"
mkdir -p "$(dirname "$input_file")"
printf '%s\n' "$command" >> "$input_file"
}
mcc-log-mcc() {
local session="${1:-}"
if [[ "$session" == "--session" ]]; then
session="${2:-}"
fi
session="$(_mcc_resolve_session "$session")"
tail -f "$(_mcc_session_log_file "$session")"
}
mcc-state() {
local session=""
while [[ $# -gt 0 ]]; do
case "$1" in
--session) session="$2"; shift 2 ;;
*) echo "Unknown option: $1" >&2; return 1 ;;
esac
done
session="$(_mcc_resolve_session "$session")"
mcc-cmd --session "$session" "debug state"
sleep 1
tail -30 "$(_mcc_session_log_file "$session")"
}
mcc-reset-session() {
local session=""
while [[ $# -gt 0 ]]; do
case "$1" in
--session) session="$2"; shift 2 ;;
*) echo "Unknown option: $1" >&2; return 1 ;;
esac
done
session="$(_mcc_resolve_session "$session")"
tmux kill-session -t "$(_mcc_tmux_session_name "$session")" 2>/dev/null || true
rm -rf "$(_mcc_session_root "$session")"
}
```
- [ ] **Step 4: 运行 smoke test确认 session input 与 reset 生效**
Run: `bash tools/test-mcc-env.sh`
Expected: PASS
- [ ] **Step 5: 提交这一小步**
```bash
git add tools/mcc-env.sh tools/test-mcc-env.sh
git commit -m "tools: scope MCC shell helpers by session"
```
### Task 3: 改造 `mcc-debug.sh``mcc-log-tail.sh` 和 kill 流程,隔离 tmux、log、config、PID、metadata
**Files:**
- Modify: `tools/mcc-debug.sh`
- Modify: `tools/mcc-log-tail.sh`
- Modify: `tools/mcc-env.sh`
- [ ] **Step 1: 用真实命令先验证当前行为会互相覆盖**
Run:
```bash
source tools/mcc-env.sh
mcc-debug -v 1.21.11 --file-input --no-build
ls -la /tmp/mcc-debug
tmux list-sessions | grep '^mcc-debug:'
```
Expected: 看到固定目录 `/tmp/mcc-debug` 和固定 tmux 名 `mcc-debug`
- [ ] **Step 2: 在 `tools/mcc-debug.sh` 中加入 `--session``--username`,并把所有运行时工件改成 session-scoped**
```bash
SESSION=""
USERNAME=""
while [[ $# -gt 0 ]]; do
case "$1" in
--session) SESSION="$2"; shift 2 ;;
--username) USERNAME="$2"; shift 2 ;;
-v|--version) VERSION="$2"; shift 2 ;;
-m|--mode) MODE="$2"; shift 2 ;;
-p|--port) PORT="$2"; PORT_SET_BY_USER=true; shift 2 ;;
--no-build) DO_BUILD=false; shift ;;
--debug-on) DEBUG_ON=true; shift ;;
--file-input) FILE_INPUT=true; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "Unknown option: $1" >&2; usage >&2; exit 1 ;;
esac
done
SESSION="$(_mcc_resolve_session "$SESSION")"
USERNAME="${USERNAME:-$(_mcc_resolve_username "$SESSION")}"
SESSION_ROOT="$(_mcc_session_root "$SESSION")"
CFG="$SESSION_ROOT/MinecraftClient.debug.ini"
MCC_LOG="$(_mcc_session_log_file "$SESSION")"
INPUT_FILE="$(_mcc_session_input_file "$SESSION")"
PID_FILE="$(_mcc_session_pid_file "$SESSION")"
META_FILE="$(_mcc_session_meta_file "$SESSION")"
MCC_TMUX_SESSION="$(_mcc_tmux_session_name "$SESSION")"
mkdir -p "$SESSION_ROOT"
cat > "$META_FILE" <<EOF
SESSION=$SESSION
USERNAME=$USERNAME
MODE=$MODE
CFG=$CFG
LOG=$MCC_LOG
INPUT=$INPUT_FILE
PID_FILE=$PID_FILE
TMUX_SESSION=$MCC_TMUX_SESSION
SERVER_VERSION=$VERSION
EOF
MCC_ARGS=("$CFG" "$USERNAME" "-" "localhost:$PORT")
```
- [ ] **Step 3: 把 `mcc-kill` 改成精准杀当前 session不再 `pkill -f MinecraftClient`**
```bash
mcc-kill() {
local session=""
while [[ $# -gt 0 ]]; do
case "$1" in
--session) session="$2"; shift 2 ;;
*) echo "Unknown option: $1" >&2; return 1 ;;
esac
done
session="$(_mcc_resolve_session "$session")"
local pid_file meta_file tmux_name
pid_file="$(_mcc_session_pid_file "$session")"
meta_file="$(_mcc_session_meta_file "$session")"
tmux_name="$(_mcc_tmux_session_name "$session")"
if [[ -f "$pid_file" ]]; then
pid="$(cat "$pid_file")"
if kill -0 "$pid" 2>/dev/null; then
kill "$pid"
wait "$pid" 2>/dev/null || true
fi
rm -f "$pid_file"
fi
tmux kill-session -t "$tmux_name" 2>/dev/null || true
[[ -f "$meta_file" ]] && rm -f "$meta_file"
}
```
- [ ] **Step 4: 让 `tools/mcc-log-tail.sh` 支持 `--session`,读取 session-specific log**
```bash
SESSION=""
while [[ $# -gt 0 ]]; do
case "$1" in
--session) SESSION="$2"; shift 2 ;;
--server) SERVER_VER="$2"; shift 2 ;;
--server-only) SERVER_ONLY=true; SERVER_VER="$2"; shift 2 ;;
-h|--help) echo "Usage: tools/mcc-log-tail.sh [--session NAME] [--server VER] [--server-only VER]"; exit 0 ;;
*) echo "Unknown option: $1"; exit 1 ;;
esac
done
SESSION="$(_mcc_resolve_session "$SESSION")"
MCC_LOG="$(_mcc_session_log_file "$SESSION")"
```
- [ ] **Step 5: 用两个显式 session 跑一次真实 smoke**
Run:
```bash
source tools/mcc-env.sh
mcc-debug -v 1.21.11 --session smoke-a --username SmokeA --file-input --no-build
mcc-debug -v 1.21.11 --session smoke-b --username SmokeB --file-input --no-build
test -f "$(_mcc_session_log_file smoke-a)"
test -f "$(_mcc_session_log_file smoke-b)"
test -f "$(_mcc_session_meta_file smoke-a)"
test -f "$(_mcc_session_meta_file smoke-b)"
tmux list-sessions | grep '^mcc-smoke-a:'
tmux list-sessions | grep '^mcc-smoke-b:'
```
Expected: 两套独立文件与两套 tmux 会话都存在
- [ ] **Step 6: 提交这一小步**
```bash
git add tools/mcc-debug.sh tools/mcc-log-tail.sh tools/mcc-env.sh
git commit -m "tools: isolate MCC debug state by session"
```
### Task 4: 加入按 worktree 选择性启用的 tmpfs 构建根,并修正外置 `obj` 的 MSBuild 过滤问题
**Files:**
- Create: `Directory.Build.props`
- Modify: `tools/mcc-env.sh`
- Test: `tools/test-mcc-env.sh`
- [ ] **Step 1: 先用当前仓库验证“直接外置 obj 会失败”,把这个回归固定住**
Run:
```bash
rm -rf /tmp/mcc-plan-build-smoke
dotnet build MinecraftClient.sln -c Release -v minimal --nologo \
-p:BaseOutputPath=/tmp/mcc-plan-build-smoke/bin/ \
-p:BaseIntermediateOutputPath=/tmp/mcc-plan-build-smoke/obj/ \
-p:MSBuildProjectExtensionsPath=/tmp/mcc-plan-build-smoke/obj/
```
Expected: FAIL包含重复 `AssemblyInfo``TargetFrameworkAttribute` 一类错误
- [ ] **Step 2: 新建 `Directory.Build.props`,统一把 `MCC_BUILD_ROOT` 接到每个项目自己的外置 `bin/obj`,并显式排除仓库内 `bin/obj`**
```xml
<Project>
<PropertyGroup>
<DefaultItemExcludes>$(DefaultItemExcludes);**/bin/**;**/obj/**</DefaultItemExcludes>
</PropertyGroup>
<PropertyGroup Condition="'$(MCC_BUILD_ROOT)' != ''">
<BaseOutputPath>$(MCC_BUILD_ROOT)/$(MSBuildProjectName)/bin/</BaseOutputPath>
<BaseIntermediateOutputPath>$(MCC_BUILD_ROOT)/$(MSBuildProjectName)/obj/</BaseIntermediateOutputPath>
<MSBuildProjectExtensionsPath>$(BaseIntermediateOutputPath)</MSBuildProjectExtensionsPath>
</PropertyGroup>
</Project>
```
- [ ] **Step 3: 在 `tools/mcc-env.sh` 中把 `mcc-build``mcc-run``mcc-tui``mcc-debug``dotnet` 调用统一接入 `MCC_BUILD_MODE=tmpfs`**
```bash
_mcc_dotnet_env() {
if [[ "${MCC_BUILD_MODE:-local}" == "tmpfs" ]]; then
local build_root
build_root="$(_mcc_build_root)"
mkdir -p "$build_root"
env MCC_BUILD_ROOT="$build_root" "$@"
return 0
fi
"$@"
}
mcc-run() {
local session="" username="" port="25565"
while [[ $# -gt 0 ]]; do
case "$1" in
--session) session="$2"; shift 2 ;;
--username) username="$2"; shift 2 ;;
--port) port="$2"; shift 2 ;;
*) break ;;
esac
done
session="$(_mcc_resolve_session "$session")"
username="${username:-$(_mcc_resolve_username "$session")}"
mkdir -p "$(_mcc_session_root "$session")"
_mcc_dotnet_env env \
MCC_FILE_INPUT=1 \
MCC_INPUT_FILE="$(_mcc_session_input_file "$session")" \
dotnet run --project "$(_mcc_repo_root)/MinecraftClient" -c Release -- \
"$(_mcc_session_root "$session")/MinecraftClient.debug.ini" \
"$username" \
- \
"localhost:$port"
}
mcc-tui() {
local session="" username="" port="25565"
while [[ $# -gt 0 ]]; do
case "$1" in
--session) session="$2"; shift 2 ;;
--username) username="$2"; shift 2 ;;
--port) port="$2"; shift 2 ;;
*) break ;;
esac
done
session="$(_mcc_resolve_session "$session")"
username="${username:-$(_mcc_resolve_username "$session")}"
tmux new-session -d -s "$(_mcc_tmux_session_name "$session")" -x 160 -y 50 \
"cd '$(_mcc_repo_root)' && MCC_BUILD_MODE='${MCC_BUILD_MODE:-local}' bash tools/mcc-debug.sh --session '$session' --username '$username' -p '$port' -m tui --no-build"
}
mcc-build-clean() {
if [[ "${MCC_BUILD_MODE:-local}" == "tmpfs" ]]; then
rm -rf "$(_mcc_build_root)"
else
dotnet clean "$(_mcc_repo_root)/MinecraftClient.sln" -c Release
fi
}
```
- [ ] **Step 4: 扩展 smoke test检查 `tmpfs` build root helper 和清理命令**
```bash
MCC_BUILD_MODE=tmpfs
build_root="$(_mcc_build_root)"
mkdir -p "$build_root/probe"
mcc-build-clean
[[ ! -e "$build_root/probe" ]]
```
- [ ] **Step 5: 用 tmpfs 模式重新 build确认能成功且产物出现在 worktree 专属构建根**
Run:
```bash
source tools/mcc-env.sh
export MCC_BUILD_MODE=tmpfs
mcc-build
find "$(_mcc_build_root)" -maxdepth 3 -type d | sed -n '1,20p'
```
Expected: `Build succeeded.`,并且构建目录位于 `/dev/shm/mcc-build/<worktree>/``/tmp/mcc-build/<worktree>/`
- [ ] **Step 6: 提交这一小步**
```bash
git add Directory.Build.props tools/mcc-env.sh tools/test-mcc-env.sh
git commit -m "build: add worktree-aware tmpfs build mode"
```
### Task 5: 更新 integration harness并新增“单服务器双会话”自动化 smoke test
**Files:**
- Create: `.skills/mcc-integration-testing/scripts/run_parallel_session_smoke_test.sh`
- Modify: `.skills/mcc-integration-testing/scripts/reset_shared_test_state.sh`
- Modify: `.skills/mcc-integration-testing/scripts/run_full_spectrum_test.sh`
- Modify: `tools/run-creative-e2e.sh`
- [ ] **Step 1: 新增并行会话 smoke test 脚本,先把预期行为写下来**
```bash
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
source "$REPO_ROOT/tools/mcc-env.sh"
source "$SCRIPT_DIR/common.sh"
VERSION="${1:-1.21.11}"
SESSION_A="parallel-a"
SESSION_B="parallel-b"
USER_A="ParallelA"
USER_B="ParallelB"
bash "$SCRIPT_DIR/preflight_test_env.sh" "$VERSION" >/dev/null
bash "$SCRIPT_DIR/reset_shared_test_state.sh" "$VERSION" >/dev/null
mcc-reset-session --session "$SESSION_A"
mcc-reset-session --session "$SESSION_B"
mcc-build
mc-start "$VERSION" >/dev/null
wait_for_server_ready "$VERSION"
mcc-debug -v "$VERSION" --session "$SESSION_A" --username "$USER_A" --file-input --no-build
mcc-debug -v "$VERSION" --session "$SESSION_B" --username "$USER_B" --file-input --no-build
grep -Fq "Server was successfully joined" "$(_mcc_session_log_file "$SESSION_A")"
grep -Fq "Server was successfully joined" "$(_mcc_session_log_file "$SESSION_B")"
mcc-cmd --session "$SESSION_A" "debug state"
mcc-cmd --session "$SESSION_B" "debug state"
mcc-kill --session "$SESSION_A"
tmux has-session -t "$(_mcc_tmux_session_name "$SESSION_B")"
mc-log "$VERSION" 50 | grep -Fq "$USER_B joined the game"
```
- [ ] **Step 2: 运行脚本,确认它先因为新参数或旧的共享路径逻辑而失败**
Run: `bash .skills/mcc-integration-testing/scripts/run_parallel_session_smoke_test.sh 1.21.11`
Expected: FAIL错误类似 `Unknown option: --session`、固定 `mcc_input.txt` 被共用,或者只有一个客户端会话存活
- [ ] **Step 3: 把 `reset_shared_test_state.sh` 缩回“只管共享服务器”,不要再删除 repo 根下 `mcc_input.txt`**
```bash
if [[ $# -eq 0 || "${1:-}" == "--all" ]]; then
while IFS= read -r session_name; do
[[ -z "$session_name" ]] && continue
kill_named_session "$session_name"
done < <(tmux list-sessions 2>/dev/null | awk -F: '/^mc-/{print $1}' || true)
while IFS= read -r pipe_path; do
[[ -z "$pipe_path" ]] && continue
if [[ ! -p "$pipe_path" ]]; then
rm -f "$pipe_path"
fi
done < <(find "$MCC_SERVERS" -maxdepth 2 -name 'stdin.pipe' 2>/dev/null || true)
fi
```
- [ ] **Step 4: 更新现有 integration 脚本,让它们使用 session-specific input/log而不是 repo 根下固定文件**
```bash
SESSION="creative-e2e-${SERVER_DIR//\//_}"
USERNAME="$(_mcc_resolve_username "$SESSION")"
INPUT_FILE="$(_mcc_session_input_file "$SESSION")"
MCC_LOG="$(_mcc_session_log_file "$SESSION")"
rm -rf "$(_mcc_session_root "$SESSION")"
mkdir -p "$(_mcc_session_root "$SESSION")"
(
cd "$REPO_ROOT"
MCC_FILE_INPUT=1 dotnet run --project MinecraftClient -c Release --no-build -- \
"$CFG" \
"$USERNAME" \
- \
"localhost:$SERVER_PORT" \
> "$MCC_LOG" 2>&1
) &
```
- [ ] **Step 5: 重新运行并行 smoke test 与现有 creative/full-spectrum 回归**
Run:
```bash
bash .skills/mcc-integration-testing/scripts/run_parallel_session_smoke_test.sh 1.21.11
bash tools/run-creative-e2e.sh 1.21.11 1.21.11 modern
bash .skills/mcc-integration-testing/scripts/run_full_spectrum_test.sh 1.21.11
```
Expected: 三个脚本都 PASS并行 smoke test 中一个 session 被 kill 后,另一个 session 和共享服务器继续存活
- [ ] **Step 6: 提交这一小步**
```bash
git add .skills/mcc-integration-testing/scripts/reset_shared_test_state.sh \
.skills/mcc-integration-testing/scripts/run_full_spectrum_test.sh \
.skills/mcc-integration-testing/scripts/run_parallel_session_smoke_test.sh \
tools/run-creative-e2e.sh
git commit -m "test: cover shared server with isolated MCC sessions"
```
### Task 6: 更新文档,并给出两个 worktree 并行调试的标准操作流程
**Files:**
- Modify: `tools/README.md`
- Modify: `docs/guide/ai-assisted-development.md`
- Modify: `.skills/mcc-dev-workflow/SKILL.md`
- [ ] **Step 1: 在文档里先补“共享服务器,隔离 MCC 会话”的核心规则**
```md
## Shared Server, Isolated MCC Sessions
- `mc-*` commands operate on the shared local Minecraft server.
- `mcc-*` commands operate on one MCC client session.
- Default `session` = current worktree name.
- Default username is derived from `session`, so two worktrees can join the same server without kicking each other.
```
- [ ] **Step 2: 在 `mcc-dev-workflow` 文档和 skill 里补两个 worktree 的真实示例**
````md
```bash
# worktree A
cd ~/Minecraft/Minecraft-Console-Client
source tools/mcc-env.sh
mc-start 1.21.11
mcc-debug -v 1.21.11 --file-input
# worktree B
cd ~/Minecraft/Minecraft-Console-Client-foo
source tools/mcc-env.sh
mcc-debug -v 1.21.11 --file-input
# Each worktree gets:
# - its own session
# - its own username
# - its own log/input/config/tmux session
# - the same shared server
```
````
- [ ] **Step 3: 在文档里加入 tmpfs 构建模式与清理命令**
````md
### tmpfs build mode
```bash
source tools/mcc-env.sh
export MCC_BUILD_MODE=tmpfs
mcc-build
mcc-build-clean
```
When `MCC_BUILD_MODE=tmpfs`, build outputs are redirected to `/dev/shm/mcc-build/<worktree>/` on Linux, or `${TMPDIR:-/tmp}/mcc-build/<worktree>/` if `/dev/shm` is unavailable.
````
- [ ] **Step 4: 跑最终验证矩阵并保存关键输出**
Run:
```bash
bash tools/test-mcc-env.sh
source tools/mcc-env.sh && unset MCC_BUILD_MODE && mcc-build
source tools/mcc-env.sh && export MCC_BUILD_MODE=tmpfs && mcc-build
bash .skills/mcc-integration-testing/scripts/run_parallel_session_smoke_test.sh 1.21.11
```
Expected:
```text
PASS
Build succeeded.
Build succeeded.
parallel session smoke: PASS
```
- [ ] **Step 5: 提交文档与最终验证结果**
```bash
git add tools/README.md docs/guide/ai-assisted-development.md .skills/mcc-dev-workflow/SKILL.md
git commit -m "docs: document shared server and isolated MCC sessions"
```
### Self-Review Checklist
- [ ] 计划里的 `session``username``MCC_BUILD_MODE``MCC_BUILD_ROOT` 命名在所有任务中一致
- [ ] 没有任何步骤重新引入 repo 根下固定 `mcc_input.txt`
- [ ] 没有任何步骤重新使用 `pkill -f "MinecraftClient"`
- [ ] 并行 smoke test 明确验证“共享服务器 + 双会话 + 单边 kill 不影响另一边”
- [ ] tmpfs build 任务明确覆盖了当前已知的 `obj` 外置重复编译问题

View file

@ -0,0 +1,316 @@
# Shared Server, Isolated MCC Sessions Design
## Goal
Keep local Minecraft server instances shared across worktrees while making MCC debug sessions fully isolated by default.
The desired workflow is:
- Multiple Git worktrees can build in parallel without output collisions.
- One shared local test server can be reused by multiple MCC clients.
- Multiple MCC debug sessions can connect to that shared server at the same time without clobbering each other's tmux sessions, logs, temp config, input file, PID tracking, or usernames.
## Background
The current repository tooling mixes two different scopes:
- Shared infrastructure state, such as local server jars and tmux server sessions
- Per-MCC-client debug state, such as `mcc_input.txt`, `/tmp/mcc-debug`, and the fixed `mcc-debug` tmux session
That works for one active workspace, but it breaks down once multiple worktrees are used at the same time.
Today the main problems are:
1. `MCC_REPO` can leak from one shell into another and point helper commands at the wrong worktree.
2. `mcc-debug.sh` writes all temp artifacts into the same `/tmp/mcc-debug` directory.
3. MCC tmux sessions use the same fixed name, `mcc-debug`.
4. MCC helper commands such as `mcc-kill` operate globally instead of targeting one debug session.
5. The default MCC username is fixed, so two clients connecting to the same server will kick each other.
6. Build outputs live inside each worktree by default, which is correct for isolation, but the workflow does not offer an intentional tmpfs-backed fast path for machines with large RAM.
## Non-Goals
- Do not isolate or duplicate server assets by worktree.
- Do not support multiple independent servers with the same version name running in parallel in this change.
- Do not redesign the Minecraft runtime or MCC account model.
- Do not make tmpfs build output mandatory.
## Design Principles
- Shared resources stay explicit and few.
- Per-session MCC state is isolated by default.
- Repo discovery is local to each script, not inherited from an ambient shell variable.
- Existing workflows should keep working with minimal changes when only one MCC session is active.
- Performance optimizations must not undermine correctness or isolation.
## State Model
### Shared State
The following remain shared across worktrees and shells:
- `MCC_SERVERS`, when explicitly set by the user
- Default server root at `<repo>/MinecraftOfficial/downloads` when `MCC_SERVERS` is not set
- Server tmux session names, still keyed by Minecraft version, for example `mc-1_21_11`
- Server stdin pipes and world data under the shared server root
- RCON access to the shared server
### Per-Session MCC State
Each MCC debug session is keyed by a session identifier.
The following must be isolated per session:
- MCC tmux session
- Temp config
- MCC log
- MCC input file
- MCC PID file
- Session metadata used by helper commands
- Default username
### Per-Worktree Build State
Each worktree gets its own build output root.
Build isolation is keyed by worktree name, not MCC session name, so multiple MCC sessions from one worktree can share the same compiled binaries.
## Naming and Identity
### Session Name
- A new `session` concept is introduced for all `mcc-*` commands.
- If the user passes `--session NAME`, that value is used.
- Otherwise the default is the current Git worktree name.
- If a worktree name cannot be resolved, the fallback is the basename of the current repo root.
### Username
- If the user passes `--username NAME`, that value is used.
- Otherwise the username is derived from the resolved `session`.
- The derived name must:
- use only Minecraft-safe characters already accepted by the existing config flow
- be deterministic
- be at most 16 characters
- remain stable for the same session across runs
Recommended derivation:
1. Lowercase the session name.
2. Replace invalid characters with underscores.
3. Prefix with `mcc_`.
4. If the result is longer than 16 characters, keep the first 11 characters and append `_` plus the first 4 hexadecimal characters of the SHA-1 of the normalized session string.
Example:
- `feature-ai` -> `mcc_feature_ai`
- `very-long-worktree-name` -> `mcc_veryl_ab12`
This truncation algorithm must be implemented and tested exactly as written so future changes do not silently rename active debug identities.
## Repo and Server Root Resolution
### Repo Root
Helper scripts must stop exporting `MCC_REPO` as shell-global state.
Instead:
- Each script resolves its own repo root from its own location.
- Shared shell helper functions may cache that resolved path internally, but not as a required ambient variable.
- Child commands should receive explicit paths or recompute the repo root themselves.
This removes the current failure mode where a shell sourced from one clone or worktree accidentally drives tools in another.
### Server Root
`MCC_SERVERS` stays as the supported override for the shared server root.
Resolution order:
1. If `MCC_SERVERS` is set, use it.
2. Otherwise use `<resolved repo root>/MinecraftOfficial/downloads`.
This keeps the useful part of the current workflow: one intentionally shared set of server assets.
## File and Process Layout
### MCC Session Root
Each MCC session gets a unique root directory:
- `${TMPDIR:-/tmp}/mcc-debug/<session>/`
That directory contains:
- `MinecraftClient.debug.ini`
- `mcc-debug.log`
- `mcc_input.txt`
- `mcc.pid`
- `session.meta`
### tmux Sessions
MCC tmux sessions must be renamed from the fixed `mcc-debug` to a session-scoped name, for example:
- `mcc-<session>`
Server tmux sessions remain version-scoped:
- `mc-<version>`
### Kill and Reset Scope
- `mcc-kill --session X` only stops the MCC process and tmux session for `X`.
- A new `mcc-reset-session` command should clear only session-scoped files and tmux sessions.
- Existing shared server reset logic must continue to operate on server state only.
- No `mcc-*` command may kill all `MinecraftClient` processes by pattern.
## Command Interface
### Shared Server Commands
These remain shared-resource commands and do not take `--session`:
- `mc-start`
- `mc-stop`
- `mc-log`
- `mc-rcon`
- `mc-wait-ready`
- `mc-wait-stop`
### Session-Scoped MCC Commands
These commands gain `--session`, and where applicable `--username`:
- `mcc-debug`
- `mcc-run`
- `mcc-tui`
- `mcc-cmd`
- `mcc-log-mcc`
- `mcc-state`
- `mcc-kill`
Expected defaults:
- `--session`: current worktree name
- `--username`: derived from session
Example commands:
```bash
mcc-debug -v 1.21.11 --session alice-a --username AliceA --file-input
mcc-debug -v 1.21.11 --session alice-b --username AliceB --file-input
mcc-cmd --session alice-a "debug state"
mcc-log-mcc --session alice-b
mcc-kill --session alice-a
mcc-reset-session --session alice-b
```
## Build Output Isolation
### Base Rule
Build outputs must be isolated by worktree, not mixed across worktrees.
This should be implemented by introducing a build root override passed through the helper scripts into `dotnet build` and `dotnet run`, without requiring users to manually edit project files per worktree.
The important behavior is:
- Worktree A writes to build root A
- Worktree B writes to build root B
- Both can build concurrently without sharing `bin/obj`
### tmpfs Acceleration
Add an opt-in tmpfs-backed build root for machines with large RAM.
Suggested layout:
- `/dev/shm/mcc-build/<worktree>/` on Linux
- fallback to `${TMPDIR:-/tmp}/mcc-build/<worktree>/` when `/dev/shm` is unavailable
Requirements:
- tmpfs mode is optional, not mandatory
- the resolved build root must be printed in debug output
- a new `mcc-build-clean` command should clear build outputs for the current worktree
- helper scripts must fail clearly if the chosen build root is not writable
### Compatibility
If tmpfs mode is disabled, behavior should remain functionally equivalent to today, except for the added worktree-aware path selection.
## Backward Compatibility
- Existing single-session workflows should still work without requiring `--session`.
- Existing external `MCC_SERVERS` setups must continue to work.
- Existing server-side helper scripts should keep their public behavior.
- Existing documentation examples can be updated gradually, but the basic commands should remain recognizable.
## Error Handling
The tooling should fail early and clearly for:
- missing server root
- invalid or empty session name
- invalid derived username after normalization
- requested tmux session already in use by a different live MCC process
- unwritable tmpfs or build root
- missing PID file on targeted kill operations
When possible, the error should print the resolved repo root, shared server root, session name, username, and session root to make misconfiguration obvious.
## Validation Plan
### Manual Verification Matrix
1. Build from two different worktrees at the same time and confirm isolated output roots.
2. Start one shared `1.21.11` server and confirm only one `mc-1_21_11` session exists.
3. Launch two MCC sessions from two different worktrees without explicit usernames and confirm distinct derived usernames.
4. Join both clients to the shared server and confirm neither client disconnects the other.
5. Send different commands through each session's input file and confirm only the intended client responds.
6. Tail each session's log and confirm no cross-session log mixing.
7. Kill one MCC session and confirm:
- the other MCC session stays connected
- the shared server remains running
8. Enable tmpfs build mode in one worktree and verify outputs are written to the resolved tmpfs path.
9. Rebuild after cleaning tmpfs outputs and confirm the worktree still functions.
### Regression Checks
- Single-session debug loop still works with no explicit `--session`
- Shared server helpers still work when `MCC_SERVERS` points outside the repo
- TUI mode still works inside tmux with session-specific naming
## Implementation Notes
The most likely files to change are:
- `tools/mcc-env.sh`
- `tools/mcc-debug.sh`
- `tools/start-server.sh`
- `.skills/mcc-integration-testing/scripts/preflight_test_env.sh`
- `.skills/mcc-integration-testing/scripts/reset_shared_test_state.sh`
- `tools/README.md`
- `docs/guide/ai-assisted-development.md`
- `.skills/mcc-dev-workflow/SKILL.md`
If build-output redirection is implemented through MSBuild configuration rather than pure shell arguments, related project or props files may also need changes.
## Open Decisions Resolved In This Design
- Keep `MCC_SERVERS`: yes
- Keep `MCC_REPO` as shell-global state: no
- Shared server instances across worktrees: yes
- MCC debug isolation keyed by explicit `session`: yes
- Default `session`: current worktree name
- Default username: derived from session
- Build output isolation key: current worktree
- tmpfs build output: opt-in optimization
## Summary
The resulting workflow intentionally shares the expensive and durable part of local MCC development, the server installation and running server instance, while isolating the volatile and user-specific part, the MCC client debug session and build outputs.
That gives parallel worktree development without forcing duplicate local servers, while removing the current collisions around shell state, tmux naming, temp files, input routing, and username reuse.

View file

@ -4,6 +4,38 @@ Scripts for analyzing Minecraft version differences and generating MCC palette f
Requires: Python 3.10+
## Local debug helpers
The `tools/` directory also contains the shell helpers used for day-to-day MCC debugging:
```bash
source tools/mcc-env.sh
mc-start 1.21.11
mcc-debug -v 1.21.11 --file-input
mcc-cmd "debug state"
```
### Shared server, isolated MCC sessions
- `mc-*` commands operate on the shared local Minecraft server.
- `mcc-*` commands operate on one MCC client session.
- The default `session` is the current worktree name.
- The default username is derived from `session`, so two worktrees can join the same shared server without kicking each other.
- `MCC_SERVERS` remains the shared server-root override.
Keep shared servers running by default. Do not stop or reset them unless the user explicitly asks for that, or you need to switch server versions.
### tmpfs build mode
```bash
source tools/mcc-env.sh
export MCC_BUILD_MODE=tmpfs
mcc-build
mcc-build-clean
```
When `MCC_BUILD_MODE=tmpfs`, build output goes to `/dev/shm/mcc-build/<worktree>/` on Linux, or `${TMPDIR:-/tmp}/mcc-build/<worktree>/` when `/dev/shm` is unavailable.
## Data Sources
Two types of data can be used as input:

View file

@ -16,6 +16,8 @@ Options:
-v, --version VER Server directory name (default: 1.21.11-Vanilla)
-m, --mode MODE Console mode: classic or tui (default: classic)
-p, --port PORT Server port (default: 25565)
--session NAME Session name for scoped runtime artifacts
--username NAME MCC username (default: resolved from session)
--no-build Skip dotnet build
--debug-on Enable debug messages from the start
--file-input Use FileInput mode (classic only; enables mcc-cmd)
@ -25,6 +27,7 @@ Examples:
tools/mcc-debug.sh # Classic mode, default server
tools/mcc-debug.sh -m tui # TUI mode
tools/mcc-debug.sh -v 1.21.11-Vanilla --debug-on
tools/mcc-debug.sh --session smoke-a --username SmokeA
tools/mcc-debug.sh --file-input # FileInput for script-driven testing
EOF
}
@ -33,15 +36,57 @@ VERSION="1.21.11-Vanilla"
MODE="classic"
PORT="25565"
PORT_SET_BY_USER=false
SESSION=""
USERNAME=""
DO_BUILD=true
DEBUG_ON=false
FILE_INPUT=false
BUILD_ROOT="$(_mcc_build_root)"
BUILD_ROOT_ENV_PREFIX=""
while [[ $# -gt 0 ]]; do
case "$1" in
-v|--version) VERSION="$2"; shift 2 ;;
-m|--mode) MODE="$2"; shift 2 ;;
-p|--port) PORT="$2"; PORT_SET_BY_USER=true; shift 2 ;;
-v|--version)
if [[ $# -lt 2 ]]; then
echo "$1 requires a value" >&2
exit 1
fi
VERSION="$2"
shift 2
;;
-m|--mode)
if [[ $# -lt 2 ]]; then
echo "$1 requires a value" >&2
exit 1
fi
MODE="$2"
shift 2
;;
-p|--port)
if [[ $# -lt 2 ]]; then
echo "$1 requires a value" >&2
exit 1
fi
PORT="$2"
PORT_SET_BY_USER=true
shift 2
;;
--session)
if [[ $# -lt 2 ]]; then
echo "--session requires a value" >&2
exit 1
fi
SESSION="$2"
shift 2
;;
--username)
if [[ $# -lt 2 ]]; then
echo "--username requires a value" >&2
exit 1
fi
USERNAME="$2"
shift 2
;;
--no-build) DO_BUILD=false; shift ;;
--debug-on) DEBUG_ON=true; shift ;;
--file-input) FILE_INPUT=true; shift ;;
@ -50,23 +95,45 @@ while [[ $# -gt 0 ]]; do
esac
done
TEST_ROOT="${TMPDIR:-/tmp}/mcc-debug"
CFG="$TEST_ROOT/MinecraftClient.debug.ini"
MCC_LOG="$TEST_ROOT/mcc-debug.log"
INPUT_FILE="$REPO_ROOT/mcc_input.txt"
SESSION="$(_mcc_resolve_session "$SESSION")"
if [[ -z "$USERNAME" ]]; then
USERNAME="$(_mcc_resolve_username "$SESSION")"
fi
if [[ "${MCC_BUILD_MODE:-local}" == "tmpfs" ]]; then
mkdir -p "$BUILD_ROOT"
printf -v BUILD_ROOT_QUOTED '%q' "$BUILD_ROOT"
BUILD_ROOT_ENV_PREFIX="MCC_BUILD_ROOT=$BUILD_ROOT_QUOTED "
fi
SESSION_ROOT="$(_mcc_session_root "$SESSION")"
CFG="$SESSION_ROOT/MinecraftClient.debug.ini"
MCC_LOG="$(_mcc_session_log_file "$SESSION")"
INPUT_FILE="$(_mcc_session_input_file "$SESSION")"
PID_FILE="$(_mcc_session_pid_file "$SESSION")"
META_FILE="$(_mcc_session_meta_file "$SESSION")"
MCC_TMUX_SESSION="$(_mcc_tmux_session_name "$SESSION")"
SESSION_NAME="mc-${VERSION//\./_}"
PREPARE_CFG_SCRIPT="$REPO_ROOT/.skills/mcc-integration-testing/scripts/prepare_offline_mcc_config.sh"
ENSURE_SERVER_SCRIPT="$REPO_ROOT/.skills/mcc-integration-testing/scripts/ensure_offline_server.sh"
PREFLIGHT_SCRIPT="$REPO_ROOT/.skills/mcc-integration-testing/scripts/preflight_test_env.sh"
GET_PORT_SCRIPT="$REPO_ROOT/.skills/mcc-integration-testing/scripts/get_server_port.sh"
mkdir -p "$TEST_ROOT"
mkdir -p "$SESSION_ROOT"
echo "=== MCC Debug Session ==="
echo " Server: $VERSION (port $PORT)"
echo " Session: $SESSION"
echo " User: $USERNAME"
echo " Mode: $MODE"
echo " Build: $BUILD_ROOT"
echo " Root: $SESSION_ROOT"
echo " Config: $CFG"
echo " Log: $MCC_LOG"
echo " Input: $INPUT_FILE"
echo " PID: $PID_FILE"
echo " Meta: $META_FILE"
echo " Tmux: $MCC_TMUX_SESSION"
echo ""
bash "$PREFLIGHT_SCRIPT" "$VERSION" >/dev/null
@ -74,7 +141,7 @@ bash "$PREFLIGHT_SCRIPT" "$VERSION" >/dev/null
# --- Build ---
if $DO_BUILD; then
echo "[1/4] Building MCC..."
dotnet build "$REPO_ROOT/MinecraftClient.sln" -c Release -v quiet --nologo
_mcc_dotnet_env dotnet build "$REPO_ROOT/MinecraftClient.sln" -c Release -v quiet --nologo
echo " Build OK"
else
echo "[1/4] Build skipped (--no-build)"
@ -82,7 +149,7 @@ fi
# --- Prepare config ---
echo "[2/4] Preparing config..."
bash "$PREPARE_CFG_SCRIPT" "$CFG" "${VERSION%-Vanilla}" CursorBot >/dev/null
bash "$PREPARE_CFG_SCRIPT" "$CFG" "${VERSION%-Vanilla}" "$USERNAME" >/dev/null
if [[ "$MODE" == "tui" ]]; then
if [[ "$(uname)" == "Darwin" ]]; then
@ -130,34 +197,71 @@ if ! $PORT_SET_BY_USER; then
PORT="$(bash "$GET_PORT_SCRIPT" "$VERSION")"
fi
RUNTIME_MODE="$MODE"
if $FILE_INPUT; then
RUNTIME_MODE="${MODE}-file-input"
fi
cat > "$META_FILE" <<EOF
session=$SESSION
user=$USERNAME
mode=$RUNTIME_MODE
config=$CFG
log=$MCC_LOG
input=$INPUT_FILE
pid=$PID_FILE
tmux=$MCC_TMUX_SESSION
server_version=$VERSION
server_port=$PORT
EOF
# --- Launch MCC ---
echo "[4/4] Launching MCC in $MODE mode..."
: > "$INPUT_FILE"
rm -f "$MCC_LOG"
rm -f "$PID_FILE"
MCC_ARGS=("$CFG" "CursorBot" "-" "localhost:$PORT")
MCC_ARGS=("$CFG" "$USERNAME" "-" "localhost:$PORT")
MCC_ARGS_CMD="$(printf '%q ' "${MCC_ARGS[@]}")"
if [[ "$MODE" == "tui" ]]; then
# TUI mode: needs a real tty — no pipes or redirects allowed
tmux kill-session -t mcc-debug 2>/dev/null || true
tmux new-session -d -s mcc-debug -x 160 -y 50 \
"cd '$REPO_ROOT' && dotnet run --project MinecraftClient -c Release --no-build -- ${MCC_ARGS[*]}; echo '=== MCC EXITED ==='; sleep 600"
# TUI mode: needs a real tty - no pipes or redirects allowed
tmux kill-session -t "$MCC_TMUX_SESSION" 2>/dev/null || true
tmux new-session -d -s "$MCC_TMUX_SESSION" -x 160 -y 50 \
"cd '$REPO_ROOT' && ${BUILD_ROOT_ENV_PREFIX}dotnet run --project MinecraftClient -c Release --no-build -- $MCC_ARGS_CMD; echo '=== MCC EXITED ==='; sleep 600"
echo ""
echo " TUI mode started in tmux session 'mcc-debug'"
echo " TUI mode started in tmux session '$MCC_TMUX_SESSION'"
echo " (TUI mode uses a real terminal; log file is not available, use MCC's /debug command)"
echo ""
echo " Attach: tmux attach -t mcc-debug"
echo " Attach: tmux attach -t $MCC_TMUX_SESSION"
echo " Detach: Ctrl+B, D"
echo " Kill MCC: tmux kill-session -t mcc-debug"
echo " Kill MCC: tmux kill-session -t $MCC_TMUX_SESSION"
echo ""
elif $FILE_INPUT; then
# FileInput mode: run in background, drive via mcc_input.txt
(
cd "$REPO_ROOT"
MCC_FILE_INPUT=1 dotnet run --project MinecraftClient -c Release --no-build -- "${MCC_ARGS[@]}" > "$MCC_LOG" 2>&1
) &
MCC_PID=$!
# FileInput mode: run in detached tmux, drive via session-specific input file
tmux kill-session -t "$MCC_TMUX_SESSION" 2>/dev/null || true
tmux new-session -d -s "$MCC_TMUX_SESSION" -x 160 -y 50 \
"cd '$REPO_ROOT' && printf '%s\n' \"\$\$\" > '$PID_FILE' && exec env ${BUILD_ROOT_ENV_PREFIX}MCC_FILE_INPUT=1 MCC_INPUT_FILE='$INPUT_FILE' dotnet run --project MinecraftClient -c Release --no-build -- $MCC_ARGS_CMD > '$MCC_LOG' 2>&1"
for _ in $(seq 1 25); do
if [[ -s "$PID_FILE" ]]; then
break
fi
sleep 0.2
done
if [[ ! -s "$PID_FILE" ]]; then
echo " Failed to capture MCC PID in $PID_FILE"
exit 1
fi
MCC_PID="$(tr -cd '0-9' < "$PID_FILE")"
if [[ -z "$MCC_PID" ]]; then
echo " Invalid PID content in $PID_FILE"
exit 1
fi
echo " MCC PID: $MCC_PID"
echo " MCC PID file: $PID_FILE"
echo ""
sleep 5
@ -175,25 +279,28 @@ elif $FILE_INPUT; then
echo ""
echo " Send commands: echo 'debug state' >> $INPUT_FILE"
echo " Tail log: tail -f $MCC_LOG"
echo " Metadata: cat $META_FILE"
echo " Attach (optional): tmux attach -t $MCC_TMUX_SESSION"
echo " Stop MCC: echo 'quit' >> $INPUT_FILE"
echo " Stop server: mc-stop $VERSION"
echo " shared servers stay up by default; rerun with --confirm only if you really need to stop it"
echo ""
else
# Interactive classic mode: run in tmux (no pipe ConsoleInteractive also needs tty)
tmux kill-session -t mcc-debug 2>/dev/null || true
tmux new-session -d -s mcc-debug -x 160 -y 50 \
"cd '$REPO_ROOT' && dotnet run --project MinecraftClient -c Release --no-build -- ${MCC_ARGS[*]}; echo '=== MCC EXITED ==='; sleep 600"
# Interactive classic mode: run in tmux (no pipe - ConsoleInteractive also needs tty)
tmux kill-session -t "$MCC_TMUX_SESSION" 2>/dev/null || true
tmux new-session -d -s "$MCC_TMUX_SESSION" -x 160 -y 50 \
"cd '$REPO_ROOT' && ${BUILD_ROOT_ENV_PREFIX}dotnet run --project MinecraftClient -c Release --no-build -- $MCC_ARGS_CMD; echo '=== MCC EXITED ==='; sleep 600"
echo ""
echo " Classic mode started in tmux session 'mcc-debug'"
echo " Classic mode started in tmux session '$MCC_TMUX_SESSION'"
echo ""
echo " Attach: tmux attach -t mcc-debug"
echo " Attach: tmux attach -t $MCC_TMUX_SESSION"
echo " Detach: Ctrl+B, D"
echo " Kill MCC: tmux kill-session -t mcc-debug"
echo " Kill MCC: tmux kill-session -t $MCC_TMUX_SESSION"
echo " Note: Use MCC's built-in /debug command or enable LogToFile for log output"
echo ""
fi
echo "Quick commands:"
echo " mc-rcon 'op CursorBot' # Give operator"
echo " mc-rcon 'op $USERNAME' # Give operator"
echo " mc-rcon 'gamemode creative' # Creative mode"
echo " mc-stop $VERSION # Stop server"
echo " mc-stop $VERSION # shared server stays up by default; rerun with --confirm only when needed"

View file

@ -12,36 +12,391 @@ else
fi
TOOLS_DIR="$(cd "$(dirname "$_mcc_env_source")" && pwd)"
MCC_REPO_ROOT="$(cd "$TOOLS_DIR/.." && pwd)"
unset _mcc_env_source
export MCC_REPO="$(cd "$TOOLS_DIR/.." && pwd)"
export MCC_SERVERS="${MCC_SERVERS:-$MCC_REPO/MinecraftOfficial/downloads}"
export MCC_REPO="$MCC_REPO_ROOT"
export MCC_SERVERS="${MCC_SERVERS:-$MCC_REPO_ROOT/MinecraftOfficial/downloads}"
_mcc_repo_root() {
printf '%s\n' "$MCC_REPO_ROOT"
}
_mcc_servers_root() {
printf '%s\n' "${MCC_SERVERS:-$MCC_REPO_ROOT/MinecraftOfficial/downloads}"
}
_mcc_current_worktree_name() {
local worktree_root
if ! worktree_root="$(git -C "$MCC_REPO_ROOT" rev-parse --show-toplevel 2>/dev/null)"; then
return 0
fi
if [[ -z "$worktree_root" ]]; then
return 0
fi
basename "$worktree_root"
}
_mcc_resolve_session() {
local explicit="${1:-}"
if [[ -n "$explicit" ]]; then
printf '%s\n' "$explicit"
return 0
fi
local worktree
worktree="$(_mcc_current_worktree_name)"
if [[ -n "$worktree" ]]; then
printf '%s\n' "$worktree"
return 0
fi
basename "$MCC_REPO_ROOT"
}
_mcc_sha1_short() {
if command -v sha1sum >/dev/null 2>&1; then
printf '%s' "$1" | sha1sum | awk '{print substr($1, 1, 4)}'
else
printf '%s' "$1" | shasum -a 1 | awk '{print substr($1, 1, 4)}'
fi
}
_mcc_resolve_username() {
local session="$1"
local normalized
normalized="$(printf '%s' "$session" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9_]/_/g')"
local candidate="mcc_${normalized}"
if (( ${#candidate} <= 16 )); then
printf '%s\n' "$candidate"
return 0
fi
local hash
hash="$(_mcc_sha1_short "$normalized")"
printf '%s_%s\n' "${candidate:0:11}" "$hash"
}
_mcc_session_root() {
printf '%s/mcc-debug/%s\n' "${TMPDIR:-/tmp}" "$1"
}
_mcc_session_log_file() {
printf '%s/mcc-debug.log\n' "$(_mcc_session_root "$1")"
}
_mcc_session_input_file() {
printf '%s/mcc_input.txt\n' "$(_mcc_session_root "$1")"
}
_mcc_session_pid_file() {
printf '%s/mcc.pid\n' "$(_mcc_session_root "$1")"
}
_mcc_session_meta_file() {
printf '%s/session.meta\n' "$(_mcc_session_root "$1")"
}
_mcc_tmux_session_name() {
printf 'mcc-%s\n' "$1"
}
_mcc_build_root() {
local worktree
worktree="$(_mcc_current_worktree_name)"
if [[ -z "$worktree" ]]; then
worktree="$(basename "$MCC_REPO_ROOT")"
fi
if [[ "${MCC_BUILD_MODE:-local}" == "tmpfs" ]]; then
if [[ -d /dev/shm && -w /dev/shm ]]; then
printf '/dev/shm/mcc-build/%s\n' "$worktree"
else
printf '%s/mcc-build/%s\n' "${TMPDIR:-/tmp}" "$worktree"
fi
return 0
fi
printf '%s\n' "$MCC_REPO_ROOT"
}
_mcc_dotnet_env() {
if [[ "${MCC_BUILD_MODE:-local}" == "tmpfs" ]]; then
local build_root
build_root="$(_mcc_build_root)"
mkdir -p "$build_root"
env MCC_BUILD_ROOT="$build_root" "$@"
return $?
fi
"$@"
}
# Helper: convert version to tmux session name (dots -> underscores)
_mc-session() { echo "mc-${1//\./_}"; }
_mc_requires_confirm() {
local action="$1"
local rerun_command="$2"
cat >&2 <<EOF
Refusing to ${action} without --confirm.
Keep shared servers running by default. Only stop or reset them when the user explicitly asks, or when you need to switch server versions.
If you really intend to do this, rerun: ${rerun_command} --confirm
EOF
return 1
}
# --- Minecraft Server Management ---
mc-start() { bash "$MCC_REPO/tools/start-server.sh" "${1:-1.20.6}"; }
mc-stop() { local v="${1:-1.20.6}"; echo "stop" > "$MCC_SERVERS/$v/stdin.pipe"; }
mc-stop() {
local v="1.20.6"
local version_set=false
local confirm=false
local -a rerun=(mc-stop)
while [[ $# -gt 0 ]]; do
case "$1" in
--confirm) confirm=true; shift ;;
*)
if [[ "$version_set" == false ]]; then
v="$1"
version_set=true
rerun+=("$1")
shift
else
echo "mc-stop: unexpected argument: $1" >&2
return 1
fi
;;
esac
done
if [[ ${#rerun[@]} -eq 1 ]]; then
rerun+=("$v")
fi
if [[ "$confirm" != true ]]; then
_mc_requires_confirm "stop shared server '$v'" "${rerun[*]}"
return 1
fi
echo "stop" > "$MCC_SERVERS/$v/stdin.pipe"
}
mc-cmd() { local v="${2:-1.20.6}"; echo "$1" > "$MCC_SERVERS/$v/stdin.pipe"; }
mc-log() { local s; s=$(_mc-session "${1:-1.20.6}"); tmux capture-pane -t "$s" -p -S "-${2:-50}"; }
mc-kill() { local v="${1:-1.20.6}" s; s=$(_mc-session "$v"); tmux kill-session -t "$s" 2>/dev/null; rm -f "$MCC_SERVERS/$v/stdin.pipe"; echo "Killed $s"; }
mc-kill() {
local v="1.20.6"
local version_set=false
local confirm=false
local -a rerun=(mc-kill)
while [[ $# -gt 0 ]]; do
case "$1" in
--confirm) confirm=true; shift ;;
*)
if [[ "$version_set" == false ]]; then
v="$1"
version_set=true
rerun+=("$1")
shift
else
echo "mc-kill: unexpected argument: $1" >&2
return 1
fi
;;
esac
done
if [[ ${#rerun[@]} -eq 1 ]]; then
rerun+=("$v")
fi
if [[ "$confirm" != true ]]; then
_mc_requires_confirm "force-kill shared server '$v'" "${rerun[*]}"
return 1
fi
local s
s=$(_mc-session "$v")
tmux kill-session -t "$s" 2>/dev/null
rm -f "$MCC_SERVERS/$v/stdin.pipe"
echo "Killed $s"
}
mc-list() { tmux list-sessions 2>/dev/null | grep "^mc-" || echo "No running MC servers"; }
mc-wait-ready() { bash "$MCC_REPO/.skills/mcc-integration-testing/scripts/preflight_test_env.sh" "${1:-1.20.6}" >/dev/null && source "$MCC_REPO/.skills/mcc-integration-testing/scripts/common.sh" && wait_for_server_ready "${1:-1.20.6}" "${2:-60}"; }
mc-wait-stop() { source "$MCC_REPO/.skills/mcc-integration-testing/scripts/common.sh" && wait_for_server_stop "${1:-1.20.6}" "${2:-60}"; }
mc-reset-test-env() { bash "$MCC_REPO/.skills/mcc-integration-testing/scripts/reset_shared_test_state.sh" "$@"; }
mc-reset-test-env() {
local confirm=false
local -a args=()
local -a rerun=(mc-reset-test-env)
while [[ $# -gt 0 ]]; do
case "$1" in
--confirm) confirm=true; shift ;;
*)
args+=("$1")
rerun+=("$1")
shift
;;
esac
done
if [[ "$confirm" != true ]]; then
_mc_requires_confirm "reset shared server test state" "${rerun[*]}"
return 1
fi
bash "$MCC_REPO/.skills/mcc-integration-testing/scripts/reset_shared_test_state.sh" "${args[@]}"
}
# --- RCON ---
mc-rcon() { bash "$MCC_REPO/tools/mc-rcon.sh" "$@"; }
# --- MCC Build/Run ---
mcc-build() { dotnet build "$MCC_REPO/MinecraftClient.sln" -c Release; }
mcc-run() {
local port="${1:-25565}"
shift || true
cd "$MCC_REPO" && MCC_FILE_INPUT=1 dotnet run --project MinecraftClient -c Release -- CursorBot - "localhost:${port}" "$@" 2>&1
mcc-build() {
local repo_root
repo_root="$(_mcc_repo_root)"
_mcc_dotnet_env dotnet build "$repo_root/MinecraftClient.sln" -c Release
}
mcc-build-clean() {
if [[ "${MCC_BUILD_MODE:-local}" == "tmpfs" ]]; then
local build_root
build_root="$(_mcc_build_root)"
rm -rf "$build_root"
return 0
fi
dotnet clean "$(_mcc_repo_root)/MinecraftClient.sln" -c Release
}
mcc-run() {
local session="" username="" port="25565"
while [[ $# -gt 0 ]]; do
case "$1" in
--session)
shift
if [[ $# -eq 0 ]]; then
echo "mcc-run: --session requires a value" >&2
return 1
fi
session="$1"
shift
;;
--username)
shift
if [[ $# -eq 0 ]]; then
echo "mcc-run: --username requires a value" >&2
return 1
fi
username="$1"
shift
;;
--port)
shift
if [[ $# -eq 0 ]]; then
echo "mcc-run: --port requires a value" >&2
return 1
fi
port="$1"
shift
;;
*)
echo "Unknown option: $1" >&2
return 1
;;
esac
done
local -a args=(--file-input --no-build --port "$port")
if [[ -n "$session" ]]; then
args+=(--session "$session")
fi
if [[ -n "$username" ]]; then
args+=(--username "$username")
fi
bash "$MCC_REPO/tools/mcc-debug.sh" "${args[@]}"
}
mcc-cmd() {
local session=""
local -a command_parts=()
while [[ $# -gt 0 ]]; do
case "$1" in
--session)
shift
if [[ $# -eq 0 ]]; then
echo "mcc-cmd: --session requires a value" >&2
return 1
fi
session="$1"
shift
;;
*)
command_parts+=("$1")
shift
;;
esac
done
if [[ ${#command_parts[@]} -eq 0 ]]; then
echo "Usage: mcc-cmd [--session NAME] <command>" >&2
return 1
fi
session="$(_mcc_resolve_session "$session")"
local input_file
input_file="$(_mcc_session_input_file "$session")"
mkdir -p "$(dirname "$input_file")"
local command
command="${command_parts[*]}"
printf '%s\n' "$command" >> "$input_file"
}
mcc-kill() {
local session=""
while [[ $# -gt 0 ]]; do
case "$1" in
--session)
shift
if [[ $# -eq 0 ]]; then
echo "mcc-kill: --session requires a value" >&2
return 1
fi
session="$1"
shift
;;
*)
echo "Unknown option: $1" >&2
return 1
;;
esac
done
session="$(_mcc_resolve_session "$session")"
local pid_file meta_file tmux_session pid pid_comm pid_args
local killed=false
pid_file="$(_mcc_session_pid_file "$session")"
meta_file="$(_mcc_session_meta_file "$session")"
tmux_session="$(_mcc_tmux_session_name "$session")"
if [[ -f "$pid_file" ]]; then
pid="$(tr -cd '0-9' < "$pid_file")"
if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
pid_comm="$(ps -p "$pid" -o comm= 2>/dev/null | tr -d '[:space:]')"
pid_args="$(ps -p "$pid" -o args= 2>/dev/null || true)"
if [[ "$pid_comm" == "MinecraftClient" ]] || { [[ "$pid_comm" == "dotnet" ]] && [[ "$pid_args" == *"MinecraftClient"* ]]; }; then
kill "$pid" 2>/dev/null || true
echo "Killed MCC PID $pid for session '$session'"
killed=true
else
echo "Refusing to kill PID $pid for session '$session': unexpected process '$pid_comm'"
fi
else
echo "No live MCC PID found for session '$session' (pid file: $pid_file)"
fi
fi
if tmux has-session -t "$tmux_session" 2>/dev/null; then
tmux kill-session -t "$tmux_session" 2>/dev/null || true
echo "Killed tmux session '$tmux_session'"
killed=true
fi
if [[ -f "$pid_file" || -f "$meta_file" ]]; then
rm -f "$pid_file" "$meta_file"
fi
if [[ "$killed" == false ]]; then
echo "No MCC process or tmux session found for session '$session'"
fi
}
mcc-cmd() { echo "$1" >> "$MCC_REPO/mcc_input.txt"; }
mcc-kill() { pkill -f "MinecraftClient" 2>/dev/null && echo "MCC killed" || echo "No MCC process found"; tmux kill-session -t mcc-debug 2>/dev/null || true; }
mcc-reload() {
mcc-kill
sleep 1
@ -50,16 +405,144 @@ mcc-reload() {
# --- TUI Mode ---
mcc-tui() {
local port="${1:-25565}"
shift || true
tmux new-session -d -s mcc-debug -x 160 -y 50 \
"cd '$MCC_REPO' && dotnet run --project MinecraftClient -c Release -- CursorBot - localhost:${port} $* 2>&1; echo '=== MCC EXITED ==='; sleep 600"
echo "TUI mode launched in tmux session 'mcc-debug'"
echo "Attach: tmux attach -t mcc-debug"
local session="" username="" port="25565"
while [[ $# -gt 0 ]]; do
case "$1" in
--session)
shift
if [[ $# -eq 0 ]]; then
echo "mcc-tui: --session requires a value" >&2
return 1
fi
session="$1"
shift
;;
--username)
shift
if [[ $# -eq 0 ]]; then
echo "mcc-tui: --username requires a value" >&2
return 1
fi
username="$1"
shift
;;
--port)
shift
if [[ $# -eq 0 ]]; then
echo "mcc-tui: --port requires a value" >&2
return 1
fi
port="$1"
shift
;;
*)
echo "Unknown option: $1" >&2
return 1
;;
esac
done
local -a args=(-m tui --no-build --port "$port")
if [[ -n "$session" ]]; then
args+=(--session "$session")
fi
if [[ -n "$username" ]]; then
args+=(--username "$username")
fi
bash "$MCC_REPO/tools/mcc-debug.sh" "${args[@]}"
}
_mcc_session_log_tail() {
local session="$1"
local log_file
log_file="$(_mcc_session_log_file "$session")"
if [[ -e "$log_file" ]]; then
tail -n 30 "$log_file" 2>/dev/null
else
echo "No MCC log found"
fi
}
_mcc_session_log_follow() {
local session="$1"
local log_file
log_file="$(_mcc_session_log_file "$session")"
tail -f "$log_file" 2>/dev/null || echo "No MCC log found"
}
# --- Debug helpers ---
mcc-debug() { bash "$MCC_REPO/tools/mcc-debug.sh" "$@"; }
mcc-log-mcc() { tail -f "${TMPDIR:-/tmp}/mcc-debug/mcc-debug.log" 2>/dev/null || echo "No MCC log found"; }
mcc-state() { echo "debug state" >> "$MCC_REPO/mcc_input.txt"; sleep 1; tail -30 "${TMPDIR:-/tmp}/mcc-debug/mcc-debug.log" 2>/dev/null; }
mcc-log-mcc() {
local session=""
while [[ $# -gt 0 ]]; do
case "$1" in
--session)
shift
if [[ $# -eq 0 ]]; then
echo "mcc-log-mcc: --session requires a value" >&2
return 1
fi
session="$1"
shift
;;
*)
echo "Unknown option: $1" >&2
return 1
;;
esac
done
session="$(_mcc_resolve_session "$session")"
_mcc_session_log_follow "$session"
}
mcc-state() {
local session=""
while [[ $# -gt 0 ]]; do
case "$1" in
--session)
shift
if [[ $# -eq 0 ]]; then
echo "mcc-state: --session requires a value" >&2
return 1
fi
session="$1"
shift
;;
*)
echo "Unknown option: $1" >&2
return 1
;;
esac
done
session="$(_mcc_resolve_session "$session")"
mcc-cmd --session "$session" "debug state"
sleep 1
_mcc_session_log_tail "$session"
}
mcc-preflight() { bash "$MCC_REPO/.skills/mcc-integration-testing/scripts/preflight_test_env.sh" "$@"; }
mcc-reset-session() {
local session=""
while [[ $# -gt 0 ]]; do
case "$1" in
--session)
shift
if [[ $# -eq 0 ]]; then
echo "mcc-reset-session: --session requires a value" >&2
return 1
fi
session="$1"
shift
;;
*)
echo "Unknown option: $1" >&2
return 1
;;
esac
done
session="$(_mcc_resolve_session "$session")"
tmux kill-session -t "$(_mcc_tmux_session_name "$session")" 2>/dev/null || true
rm -rf "$(_mcc_session_root "$session")"
}

View file

@ -2,6 +2,7 @@
# Tail MCC and/or server logs side-by-side or individually.
# Usage:
# tools/mcc-log-tail.sh # tail MCC log only
# tools/mcc-log-tail.sh --session NAME # tail MCC log for a specific session
# tools/mcc-log-tail.sh --server VER # tail both MCC and server logs
# tools/mcc-log-tail.sh --server-only VER # tail server log only
set -euo pipefail
@ -11,21 +12,47 @@ REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
# shellcheck source=tools/mcc-env.sh
source "$REPO_ROOT/tools/mcc-env.sh"
MCC_LOG="${TMPDIR:-/tmp}/mcc-debug/mcc-debug.log"
SESSION=""
SERVER_VER=""
SERVER_ONLY=false
while [[ $# -gt 0 ]]; do
case "$1" in
--server) SERVER_VER="$2"; shift 2 ;;
--server-only) SERVER_ONLY=true; SERVER_VER="$2"; shift 2 ;;
--session)
if [[ $# -lt 2 ]]; then
echo "--session requires a value" >&2
exit 1
fi
SESSION="$2"
shift 2
;;
--server)
if [[ $# -lt 2 ]]; then
echo "--server requires a value" >&2
exit 1
fi
SERVER_VER="$2"
shift 2
;;
--server-only)
if [[ $# -lt 2 ]]; then
echo "--server-only requires a value" >&2
exit 1
fi
SERVER_ONLY=true
SERVER_VER="$2"
shift 2
;;
-h|--help)
echo "Usage: tools/mcc-log-tail.sh [--server VER] [--server-only VER]"
echo "Usage: tools/mcc-log-tail.sh [--session NAME] [--server VER] [--server-only VER]"
exit 0 ;;
*) echo "Unknown option: $1"; exit 1 ;;
esac
done
SESSION="$(_mcc_resolve_session "$SESSION")"
MCC_LOG="$(_mcc_session_log_file "$SESSION")"
if $SERVER_ONLY; then
if [[ -z "$SERVER_VER" ]]; then
echo "Specify server version with --server-only VER" >&2
@ -39,16 +66,17 @@ fi
if [[ -n "$SERVER_VER" ]]; then
SERVER_LOG="$MCC_SERVERS/$SERVER_VER/logs/latest.log"
echo "=== Tailing MCC + server logs ==="
echo " Session: $SESSION"
echo " MCC: $MCC_LOG"
echo " Server: $SERVER_LOG"
echo ""
tail -f "$MCC_LOG" "$SERVER_LOG" 2>/dev/null
else
if [[ ! -f "$MCC_LOG" ]]; then
echo "No MCC log found at $MCC_LOG"
echo "Start MCC first with: tools/mcc-debug.sh --file-input"
echo "No MCC log found for session '$SESSION' at $MCC_LOG"
echo "Start MCC first with: tools/mcc-debug.sh --session $SESSION --file-input"
exit 1
fi
echo "=== Tailing MCC log: $MCC_LOG ==="
echo "=== Tailing MCC log for session '$SESSION': $MCC_LOG ==="
exec tail -f "$MCC_LOG"
fi

View file

@ -36,10 +36,13 @@ fi
SESSION_NAME="mc-${SERVER_DIR//./_}"
TEST_ROOT="${TMPDIR:-/tmp}/mcc-creative-e2e/${SERVER_DIR//\//_}"
CFG="$TEST_ROOT/MinecraftClient.$MC_VERSION.ini"
MCC_LOG="$TEST_ROOT/mcc.log"
MCC_SESSION="creative-e2e-${SERVER_DIR//[^a-zA-Z0-9]/_}-${PROFILE}"
TEST_USERNAME="$(_mcc_resolve_username "$MCC_SESSION")"
MCC_LOG="$(_mcc_session_log_file "$MCC_SESSION")"
PID_FILE="$(_mcc_session_pid_file "$MCC_SESSION")"
MCC_TMUX_SESSION="$(_mcc_tmux_session_name "$MCC_SESSION")"
SERVER_LOG_FILE="$MCC_SERVERS/$SERVER_DIR/logs/latest.log"
INPUT_FILE="$REPO_ROOT/mcc_input.txt"
MCC_PID=""
INPUT_FILE="$(_mcc_session_input_file "$MCC_SESSION")"
SERVER_PORT="25565"
mkdir -p "$TEST_ROOT"
@ -63,12 +66,41 @@ wait_for_file_pattern() {
return 1
}
port_is_listening() {
local port="$1"
if ! command -v python3 >/dev/null 2>&1; then
return 1
fi
python3 - "$port" <<'PY'
import socket
import sys
port = int(sys.argv[1])
for addrinfo in socket.getaddrinfo("localhost", port, 0, socket.SOCK_STREAM):
family, socktype, proto, _, sockaddr = addrinfo
try:
sock = socket.socket(family, socktype, proto)
sock.settimeout(0.2)
if sock.connect_ex(sockaddr) == 0:
sock.close()
raise SystemExit(0)
sock.close()
except OSError:
continue
raise SystemExit(1)
PY
}
wait_for_rcon_port_free() {
local timeout="${1:-30}"
local elapsed=0
while (( elapsed < timeout )); do
if ! ss -ltn '( sport = :25575 )' 2>/dev/null | grep -Fq ':25575'; then
if ! port_is_listening 25575; then
return 0
fi
sleep 1
@ -79,18 +111,16 @@ wait_for_rcon_port_free() {
return 1
}
cleanup() {
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
mcc-cmd --session "$MCC_SESSION" "quit" >/dev/null 2>&1 || true
sleep 2
mcc-kill --session "$MCC_SESSION" >/dev/null 2>&1 || true
if [[ -p "$MCC_SERVERS/$SERVER_DIR/stdin.pipe" ]]; then
echo "stop" > "$MCC_SERVERS/$SERVER_DIR/stdin.pipe" 2>/dev/null || true
if tmux has-session -t "$SESSION_NAME" 2>/dev/null; then
tmux send-keys -t "$SESSION_NAME" "stop" C-m >/dev/null 2>&1 || true
wait_for_server_stop "$SERVER_DIR" 20 >/dev/null 2>&1 || true
fi
rm -f "$MCC_SERVERS/$SERVER_DIR/stdin.pipe"
tmux kill-session -t "$SESSION_NAME" 2>/dev/null || true
wait_for_rcon_port_free 30 || true
}
@ -100,7 +130,7 @@ trap cleanup EXIT
prepare_config() {
MCC_TEST_ACCOUNT_TYPE=mojang MCC_TEST_PASSWORD=- \
bash "$REPO_ROOT/.skills/mcc-integration-testing/scripts/prepare_offline_mcc_config.sh" \
"$REPO_ROOT/MinecraftClient.ini" "$CFG" "$MC_VERSION" CursorBot >/dev/null
"$CFG" "$MC_VERSION" "$TEST_USERNAME" >/dev/null
sed_in_place \
-e "s#^Server = .*#Server = { Host = \"localhost\", Port = $SERVER_PORT }#" \
@ -115,7 +145,7 @@ prepare_config() {
send_mcc_command() {
local command="$1"
local delay="${2:-2}"
echo "$command" >> "$INPUT_FILE"
mcc-cmd --session "$MCC_SESSION" "$command"
sleep "$delay"
}
@ -147,13 +177,34 @@ assert_log_contains() {
wait_for_file_pattern "$file" "$pattern" "$description" "$timeout"
}
start_mcc_session() {
local -a mcc_args=("$CFG" "$TEST_USERNAME" "-" "localhost:$SERVER_PORT")
local mcc_args_cmd
mcc_args_cmd="$(printf '%q ' "${mcc_args[@]}")"
tmux kill-session -t "$MCC_TMUX_SESSION" 2>/dev/null || true
rm -f "$PID_FILE"
tmux new-session -d -s "$MCC_TMUX_SESSION" -x 160 -y 50 \
"cd '$REPO_ROOT' && printf '%s\n' \"\$\$\" > '$PID_FILE' && exec env MCC_FILE_INPUT=1 MCC_INPUT_FILE='$INPUT_FILE' dotnet run --project MinecraftClient -c Release --no-build -- $mcc_args_cmd > '$MCC_LOG' 2>&1"
for _ in $(seq 1 25); do
if [[ -s "$PID_FILE" ]]; then
return 0
fi
sleep 0.2
done
echo "Failed to capture MCC PID for session $MCC_SESSION" >&2
return 1
}
legacy_server_setup() {
run_server_command "gamerule sendCommandFeedback true"
run_server_command "time set day"
run_server_command "weather clear"
run_server_command "gamemode creative CursorBot"
run_server_command "gamemode creative $TEST_USERNAME"
run_server_command "fill -2 79 -2 2 79 2 stone"
run_server_command "tp CursorBot 0 80 0"
run_server_command "tp $TEST_USERNAME 0 80 0"
}
modern_server_setup() {
@ -161,30 +212,32 @@ modern_server_setup() {
run_server_command "gamerule logAdminCommands true"
run_server_command "time set day"
run_server_command "weather clear"
run_server_command "gamemode creative CursorBot"
run_server_command "gamemode creative $TEST_USERNAME"
run_server_command "fill -2 79 -2 2 79 2 stone"
run_server_command "tp CursorBot 0 80 0"
run_server_command "tp $TEST_USERNAME 0 80 0"
}
legacy_mob_and_effects() {
run_server_command "summon Cow 2 80 0"
run_server_command "summon Zombie 4 80 0"
run_server_command "summon Pig -2 80 0"
run_server_command "effect CursorBot 1 30 1 true"
run_server_command "effect CursorBot 10 10 1 true"
run_server_command "effect $TEST_USERNAME 1 30 1 true"
run_server_command "effect $TEST_USERNAME 10 10 1 true"
}
modern_mob_and_effects() {
run_server_command "summon minecraft:cow 2 80 0"
run_server_command "summon minecraft:zombie 4 80 0"
run_server_command "summon minecraft:pig -2 80 0"
run_server_command "effect give CursorBot minecraft:speed 30 1 true"
run_server_command "effect give CursorBot minecraft:regeneration 10 1 true"
run_server_command "effect give $TEST_USERNAME minecraft:speed 30 1 true"
run_server_command "effect give $TEST_USERNAME minecraft:regeneration 10 1 true"
}
bash "$REPO_ROOT/.skills/mcc-integration-testing/scripts/preflight_test_env.sh" "$SERVER_DIR" >/dev/null
bash "$REPO_ROOT/.skills/mcc-integration-testing/scripts/reset_shared_test_state.sh" --all >/dev/null
bash "$REPO_ROOT/.skills/mcc-integration-testing/scripts/reset_shared_test_state.sh" "$SERVER_DIR" >/dev/null
mcc-reset-session --session "$MCC_SESSION" >/dev/null
wait_for_rcon_port_free 30 || true
mkdir -p "$(dirname "$MCC_LOG")" "$(dirname "$INPUT_FILE")"
rm -f "$MCC_LOG" "$INPUT_FILE"
bash "$REPO_ROOT/.skills/mcc-integration-testing/scripts/ensure_offline_server.sh" "$SERVER_DIR" >/dev/null
@ -199,22 +252,13 @@ prepare_config
: > "$INPUT_FILE"
(
cd "$REPO_ROOT"
MCC_FILE_INPUT=1 dotnet run --project MinecraftClient -c Release --no-build -- \
"$CFG" \
CursorBot \
- \
"localhost:$SERVER_PORT" \
> "$MCC_LOG" 2>&1
) &
MCC_PID=$!
start_mcc_session
assert_log_contains "$MCC_LOG" "Server was successfully joined." "MCC join success" 90
assert_log_contains "$SERVER_LOG_FILE" "CursorBot joined the game" "server join entry" 30
assert_log_contains "$SERVER_LOG_FILE" "$TEST_USERNAME joined the game" "server join entry" 30
print_phase "CONNECT" "PASS"
run_server_command "op CursorBot"
run_server_command "op $TEST_USERNAME"
sleep 1
if [[ "$PROFILE" == "legacy" ]]; then
@ -237,7 +281,7 @@ assert_log_contains "$SERVER_LOG_FILE" "$cmd_token" "client command on server" 2
print_phase "SEND" "PASS"
run_server_command "say $broadcast_token"
run_server_command "tell CursorBot $whisper_token"
run_server_command "tell $TEST_USERNAME $whisper_token"
assert_log_contains "$MCC_LOG" "$broadcast_token" "server broadcast in MCC" 20
assert_log_contains "$MCC_LOG" "$whisper_token" "server whisper in MCC" 20
print_phase "RECEIVE" "PASS"

140
tools/test-mcc-env.sh Executable file
View file

@ -0,0 +1,140 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
source "$REPO_ROOT/tools/mcc-env.sh"
assert_eq() {
local expected="$1"
local actual="$2"
local label="$3"
if [[ "$expected" != "$actual" ]]; then
echo "FAIL: $label" >&2
echo " expected: $expected" >&2
echo " actual: $actual" >&2
exit 1
fi
}
assert_regex() {
local regex="$1"
local actual="$2"
local label="$3"
if [[ ! "$actual" =~ $regex ]]; then
echo "FAIL: $label" >&2
echo " regex: $regex" >&2
echo " actual: $actual" >&2
exit 1
fi
}
tmpfs_build_base() {
if [[ -d /dev/shm && -w /dev/shm ]]; then
printf '/dev/shm'
else
printf '%s' "${TMPDIR:-/tmp}"
fi
}
session="$(_mcc_resolve_session "demo-branch")"
assert_eq "demo-branch" "$session" "explicit session"
short_name="$(_mcc_resolve_username "feature-ai")"
assert_eq "mcc_feature_ai" "$short_name" "short derived username"
long_name="$(_mcc_resolve_username "very-long-worktree-name")"
assert_regex '^mcc_[a-z0-9_]{7}_[0-9a-f]{4}$' "$long_name" "long derived username shape"
assert_eq "16" "${#long_name}" "long derived username length"
assert_eq "${TMPDIR:-/tmp}/mcc-debug/demo-branch" "$(_mcc_session_root "demo-branch")" "session root"
assert_eq "mcc-demo-branch" "$(_mcc_tmux_session_name "demo-branch")" "tmux session name"
MCC_BUILD_MODE=tmpfs
original_repo_root="$MCC_REPO_ROOT"
fallback_root="$REPO_ROOT/nonexistent-worktree"
MCC_REPO_ROOT="$fallback_root"
fallback_session="$(_mcc_resolve_session)"
assert_eq "$(basename "$fallback_root")" "$fallback_session" "session fallback without git"
fallback_build_root="$(_mcc_build_root)"
tmpfs_base="$(tmpfs_build_base)"
expected_fallback_root="$tmpfs_base/mcc-build/$(basename "$fallback_root")"
assert_eq "$expected_fallback_root" "$fallback_build_root" "tmpfs build root fallback"
MCC_REPO_ROOT="$original_repo_root"
build_root="$(_mcc_build_root)"
expected_prefix="$(tmpfs_build_base)/mcc-build/"
if [[ "$build_root" != "$expected_prefix"* ]]; then
echo "FAIL: tmpfs build root" >&2
echo " expected prefix: $expected_prefix" >&2
echo " actual: $build_root" >&2
exit 1
fi
dotnet_env_build_root="$(_mcc_dotnet_env env | awk -F= '$1=="MCC_BUILD_ROOT" { print $2 }')"
assert_eq "$build_root" "$dotnet_env_build_root" "dotnet env wrapper exports MCC_BUILD_ROOT"
mkdir -p "$build_root/probe"
mcc-build-clean
[[ ! -e "$build_root/probe" ]]
session="wrapper-smoke"
input_file="$(_mcc_session_input_file "$session")"
rm -rf "$(_mcc_session_root "$session")"
mcc-cmd --session "$session" debug state
input_contents="$(cat "$input_file")"
assert_eq "debug state" "$input_contents" "session input command is intact"
mcc-reset-session --session "$session"
[[ ! -e "$(_mcc_session_root "$session")" ]]
malformed_log="${TMPDIR:-/tmp}/mcc-env-session-hang-test.log"
for func in mcc-cmd mcc-reset-session mcc-state mcc-log-mcc mcc-run mcc-tui; do
set +e
"$func" --session >"$malformed_log" 2>&1
status=$?
set -e
if [[ $status -eq 0 ]]; then
echo "FAIL: $func accepted --session without a value" >&2
cat "$malformed_log" >&2
exit 1
fi
grep -Fq -- "--session requires a value" "$malformed_log"
done
guard_root="$(mktemp -d "${TMPDIR:-/tmp}/mcc-env-guard.XXXXXX")"
MCC_SERVERS="$guard_root"
mkdir -p "$MCC_SERVERS/testver"
printf 'unchanged\n' > "$MCC_SERVERS/testver/stdin.pipe"
confirm_log="${TMPDIR:-/tmp}/mcc-env-confirm-guard.log"
for cmd in \
"mc-stop testver" \
"mc-kill testver" \
"mc-reset-test-env testver"
do
set +e
eval "$cmd" >"$confirm_log" 2>&1
status=$?
set -e
if [[ $status -eq 0 ]]; then
echo "FAIL: $cmd ran without --confirm" >&2
cat "$confirm_log" >&2
exit 1
fi
grep -Fq -- "--confirm" "$confirm_log"
grep -Fq "Keep shared servers running by default" "$confirm_log"
done
stdin_contents="$(cat "$MCC_SERVERS/testver/stdin.pipe")"
assert_eq "unchanged" "$stdin_contents" "mc-stop without confirm does not touch stdin pipe"
: > "$MCC_SERVERS/testver/stdin.pipe"
mc-stop testver --confirm
assert_eq "stop" "$(cat "$MCC_SERVERS/testver/stdin.pipe")" "mc-stop with confirm writes to stdin pipe"
echo "PASS"