From 158a4b1b13053e02a4a224adafb7205925c3c38a Mon Sep 17 00:00:00 2001 From: BruceChen Date: Sun, 12 Apr 2026 17:37:13 +0800 Subject: [PATCH 01/18] docs: add MCC worktree session design --- ...red-server-isolated-mcc-sessions-design.md | 316 ++++++++++++++++++ 1 file changed, 316 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-12-shared-server-isolated-mcc-sessions-design.md diff --git a/docs/superpowers/specs/2026-04-12-shared-server-isolated-mcc-sessions-design.md b/docs/superpowers/specs/2026-04-12-shared-server-isolated-mcc-sessions-design.md new file mode 100644 index 00000000..9fc0e376 --- /dev/null +++ b/docs/superpowers/specs/2026-04-12-shared-server-isolated-mcc-sessions-design.md @@ -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 `/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 `/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//` + +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-` + +Server tmux sessions remain version-scoped: + +- `mc-` + +### 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//` on Linux +- fallback to `${TMPDIR:-/tmp}/mcc-build//` 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. From 6b986dcddddb8b9888e741d3f4a18fea19f0b673 Mon Sep 17 00:00:00 2001 From: BruceChen Date: Sun, 12 Apr 2026 18:46:57 +0800 Subject: [PATCH 02/18] chore: update ModelContextProtocol package version to 1.2.0 and adjust Consolonia and Sentry package versions --- DebugTools/MccMcpStdioHarness/MccMcpStdioHarness.csproj | 2 +- DebugTools/MccMcpWebPlayground/MccMcpWebPlayground.csproj | 2 +- MinecraftClient/MinecraftClient.csproj | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/DebugTools/MccMcpStdioHarness/MccMcpStdioHarness.csproj b/DebugTools/MccMcpStdioHarness/MccMcpStdioHarness.csproj index 2261edaa..5bbe1b4e 100644 --- a/DebugTools/MccMcpStdioHarness/MccMcpStdioHarness.csproj +++ b/DebugTools/MccMcpStdioHarness/MccMcpStdioHarness.csproj @@ -8,7 +8,7 @@ - + diff --git a/DebugTools/MccMcpWebPlayground/MccMcpWebPlayground.csproj b/DebugTools/MccMcpWebPlayground/MccMcpWebPlayground.csproj index 0ee15b7a..96c52c1a 100644 --- a/DebugTools/MccMcpWebPlayground/MccMcpWebPlayground.csproj +++ b/DebugTools/MccMcpWebPlayground/MccMcpWebPlayground.csproj @@ -8,7 +8,7 @@ - + diff --git a/MinecraftClient/MinecraftClient.csproj b/MinecraftClient/MinecraftClient.csproj index fadb2203..9a9c8baa 100644 --- a/MinecraftClient/MinecraftClient.csproj +++ b/MinecraftClient/MinecraftClient.csproj @@ -38,20 +38,20 @@ - + - - + + - + NU1701 From 3b65085a669f1ac43efd53b72f2dd38c0214271a Mon Sep 17 00:00:00 2001 From: BruceChen Date: Sun, 12 Apr 2026 18:52:17 +0800 Subject: [PATCH 03/18] tools: add MCC session and build root helpers --- tools/mcc-env.sh | 100 +++++++++++++++++++++++++++++++++++++++++- tools/test-mcc-env.sh | 49 +++++++++++++++++++++ 2 files changed, 147 insertions(+), 2 deletions(-) create mode 100755 tools/test-mcc-env.sh diff --git a/tools/mcc-env.sh b/tools/mcc-env.sh index 904a8a00..490bf10b 100644 --- a/tools/mcc-env.sh +++ b/tools/mcc-env.sh @@ -12,9 +12,105 @@ 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() { + 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" + 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" +} # Helper: convert version to tmux session name (dots -> underscores) _mc-session() { echo "mc-${1//\./_}"; } diff --git a/tools/test-mcc-env.sh b/tools/test-mcc-env.sh new file mode 100755 index 00000000..73278e1f --- /dev/null +++ b/tools/test-mcc-env.sh @@ -0,0 +1,49 @@ +#!/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" From fdb6e5db4cf31813c60a131bf4194c4982a2379a Mon Sep 17 00:00:00 2001 From: BruceChen Date: Sun, 12 Apr 2026 19:00:42 +0800 Subject: [PATCH 04/18] tools: harden session helper and tests --- tools/mcc-env.sh | 11 ++++++++++- tools/test-mcc-env.sh | 13 +++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/tools/mcc-env.sh b/tools/mcc-env.sh index 490bf10b..505b0ea3 100644 --- a/tools/mcc-env.sh +++ b/tools/mcc-env.sh @@ -26,7 +26,16 @@ _mcc_servers_root() { } _mcc_current_worktree_name() { - git -C "$MCC_REPO_ROOT" rev-parse --show-toplevel 2>/dev/null | xargs basename + 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() { diff --git a/tools/test-mcc-env.sh b/tools/test-mcc-env.sh index 73278e1f..6fb9957d 100755 --- a/tools/test-mcc-env.sh +++ b/tools/test-mcc-env.sh @@ -43,6 +43,19 @@ assert_eq "${TMPDIR:-/tmp}/mcc-debug/demo-branch" "$(_mcc_session_root "demo-bra 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)" +assert_regex "^(/dev/shm|/tmp)/mcc-build/$(basename "$fallback_root")\$" "$fallback_build_root" "tmpfs build root fallback" + +MCC_REPO_ROOT="$original_repo_root" + build_root="$(_mcc_build_root)" assert_regex '^(/dev/shm|/tmp)/mcc-build/.+$' "$build_root" "tmpfs build root" From d9c6826443b782ba8d2abec201ecfc7cf0698351 Mon Sep 17 00:00:00 2001 From: BruceChen Date: Sun, 12 Apr 2026 19:04:38 +0800 Subject: [PATCH 05/18] tools: fix tmpfs build root test --- tools/test-mcc-env.sh | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/tools/test-mcc-env.sh b/tools/test-mcc-env.sh index 6fb9957d..9e2b5f7b 100755 --- a/tools/test-mcc-env.sh +++ b/tools/test-mcc-env.sh @@ -29,6 +29,14 @@ assert_regex() { 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" @@ -52,11 +60,19 @@ fallback_session="$(_mcc_resolve_session)" assert_eq "$(basename "$fallback_root")" "$fallback_session" "session fallback without git" fallback_build_root="$(_mcc_build_root)" -assert_regex "^(/dev/shm|/tmp)/mcc-build/$(basename "$fallback_root")\$" "$fallback_build_root" "tmpfs build root fallback" +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)" -assert_regex '^(/dev/shm|/tmp)/mcc-build/.+$' "$build_root" "tmpfs 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 echo "PASS" From 4c6d5b566b7fa34c9d1f8f5a56181a16cb541614 Mon Sep 17 00:00:00 2001 From: BruceChen Date: Sun, 12 Apr 2026 19:10:00 +0800 Subject: [PATCH 06/18] Make MCC helpers session-aware and add reset test --- tools/mcc-env.sh | 60 ++++++++++++++++++++++++++++++++++++++++--- tools/test-mcc-env.sh | 10 ++++++++ 2 files changed, 67 insertions(+), 3 deletions(-) diff --git a/tools/mcc-env.sh b/tools/mcc-env.sh index 505b0ea3..0e1ddc8e 100644 --- a/tools/mcc-env.sh +++ b/tools/mcc-env.sh @@ -139,13 +139,40 @@ mc-reset-test-env() { bash "$MCC_REPO/.skills/mcc-integration-testing/scripts/re mc-rcon() { bash "$MCC_REPO/tools/mc-rcon.sh" "$@"; } # --- MCC Build/Run --- -mcc-build() { dotnet build "$MCC_REPO/MinecraftClient.sln" -c Release; } +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-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-cmd() { echo "$1" >> "$MCC_REPO/mcc_input.txt"; } +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] " >&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-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 @@ -163,8 +190,35 @@ mcc-tui() { echo "Attach: tmux attach -t mcc-debug" } +_mcc_session_log_tail() { + 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-log-mcc() { + local session="${1:-}" + if [[ "$session" == "--session" ]]; then + session="${2:-}" + fi + session="$(_mcc_resolve_session "$session")" + _mcc_session_log_tail "$session" +} 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-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) 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")" +} diff --git a/tools/test-mcc-env.sh b/tools/test-mcc-env.sh index 9e2b5f7b..2d2fb284 100755 --- a/tools/test-mcc-env.sh +++ b/tools/test-mcc-env.sh @@ -75,4 +75,14 @@ if [[ "$build_root" != "$expected_prefix"* ]]; then exit 1 fi +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")" ]] + echo "PASS" From 1be001bdf8059d1427918f0d140a53138568d64e Mon Sep 17 00:00:00 2001 From: BruceChen Date: Sun, 12 Apr 2026 19:21:07 +0800 Subject: [PATCH 07/18] Fix session parser robustness and test assertion --- tools/mcc-env.sh | 90 ++++++++++++++++++++++++++++++++++++++----- tools/test-mcc-env.sh | 7 ++++ 2 files changed, 88 insertions(+), 9 deletions(-) diff --git a/tools/mcc-env.sh b/tools/mcc-env.sh index 0e1ddc8e..28378c9a 100644 --- a/tools/mcc-env.sh +++ b/tools/mcc-env.sh @@ -161,8 +161,19 @@ mcc-cmd() { local command="" while [[ $# -gt 0 ]]; do case "$1" in - --session) session="$2"; shift 2 ;; - *) command="$1"; shift ;; + --session) + shift + if [[ $# -eq 0 ]]; then + echo "mcc-cmd: --session requires a value" >&2 + return 1 + fi + session="$1" + shift + ;; + *) + command="$1" + shift + ;; esac done @@ -191,6 +202,17 @@ mcc-tui() { } _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")" @@ -200,21 +222,71 @@ _mcc_session_log_tail() { # --- Debug helpers --- mcc-debug() { bash "$MCC_REPO/tools/mcc-debug.sh" "$@"; } mcc-log-mcc() { - local session="${1:-}" - if [[ "$session" == "--session" ]]; then - session="${2:-}" - fi + 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-state() { echo "debug state" >> "$MCC_REPO/mcc_input.txt"; sleep 1; tail -30 "${TMPDIR:-/tmp}/mcc-debug/mcc-debug.log" 2>/dev/null; } 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) session="$2"; shift 2 ;; - *) echo "Unknown option: $1" >&2; return 1 ;; + --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 diff --git a/tools/test-mcc-env.sh b/tools/test-mcc-env.sh index 2d2fb284..e54ade93 100755 --- a/tools/test-mcc-env.sh +++ b/tools/test-mcc-env.sh @@ -84,5 +84,12 @@ grep -Fq "debug state" "$input_file" mcc-reset-session --session "$session" [[ ! -e "$(_mcc_session_root "$session")" ]] +malformed_log="${TMPDIR:-/tmp}/mcc-env-session-hang-test.log" +if mcc-cmd --session >"$malformed_log" 2>&1; then + echo "FAIL: --session accepted without value" >&2 + cat "$malformed_log" >&2 + exit 1 +fi +grep -Fq -- "--session requires a value" "$malformed_log" echo "PASS" From e03b7de959844432ee27bbfab7fc2c46bdd3dee5 Mon Sep 17 00:00:00 2001 From: BruceChen Date: Sun, 12 Apr 2026 19:25:25 +0800 Subject: [PATCH 08/18] Preserve full command in mcc-cmd and broaden tests --- tools/mcc-env.sh | 11 ++++++++--- tools/test-mcc-env.sh | 21 ++++++++++++++------- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/tools/mcc-env.sh b/tools/mcc-env.sh index 28378c9a..eb735894 100644 --- a/tools/mcc-env.sh +++ b/tools/mcc-env.sh @@ -158,7 +158,7 @@ mcc-run() { } mcc-cmd() { local session="" - local command="" + local -a command_parts=() while [[ $# -gt 0 ]]; do case "$1" in --session) @@ -171,17 +171,22 @@ mcc-cmd() { shift ;; *) - command="$1" + command_parts+=("$1") shift ;; esac done - [[ -n "$command" ]] || { echo "Usage: mcc-cmd [--session NAME] " >&2; return 1; } + if [[ ${#command_parts[@]} -eq 0 ]]; then + echo "Usage: mcc-cmd [--session NAME] " >&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() { pkill -f "MinecraftClient" 2>/dev/null && echo "MCC killed" || echo "No MCC process found"; tmux kill-session -t mcc-debug 2>/dev/null || true; } diff --git a/tools/test-mcc-env.sh b/tools/test-mcc-env.sh index e54ade93..b0bfb18b 100755 --- a/tools/test-mcc-env.sh +++ b/tools/test-mcc-env.sh @@ -80,16 +80,23 @@ 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" +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" -if mcc-cmd --session >"$malformed_log" 2>&1; then - echo "FAIL: --session accepted without value" >&2 - cat "$malformed_log" >&2 - exit 1 -fi -grep -Fq -- "--session requires a value" "$malformed_log" +for func in mcc-cmd mcc-reset-session mcc-state mcc-log-mcc; 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 echo "PASS" From afee31e9188a2343ecc1291cbca9c25657033b71 Mon Sep 17 00:00:00 2001 From: BruceChen Date: Sun, 12 Apr 2026 19:26:32 +0800 Subject: [PATCH 09/18] Ensure mcc-cmd handles unquoted multi-token commands --- tools/test-mcc-env.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/test-mcc-env.sh b/tools/test-mcc-env.sh index b0bfb18b..2b8e1563 100755 --- a/tools/test-mcc-env.sh +++ b/tools/test-mcc-env.sh @@ -79,7 +79,7 @@ session="wrapper-smoke" input_file="$(_mcc_session_input_file "$session")" rm -rf "$(_mcc_session_root "$session")" -mcc-cmd --session "$session" "debug state" +mcc-cmd --session "$session" debug state input_contents="$(cat "$input_file")" assert_eq "debug state" "$input_contents" "session input command is intact" From bf292ea1be0caecbd928bfbba651e8508194d212 Mon Sep 17 00:00:00 2001 From: BruceChen Date: Sun, 12 Apr 2026 19:38:53 +0800 Subject: [PATCH 10/18] tools: scope mcc debug runtime artifacts by session --- tools/mcc-debug.sh | 88 ++++++++++++++++++++++++++++++++----------- tools/mcc-env.sh | 53 +++++++++++++++++++++++++- tools/mcc-log-tail.sh | 16 +++++--- 3 files changed, 129 insertions(+), 28 deletions(-) diff --git a/tools/mcc-debug.sh b/tools/mcc-debug.sh index c5c6205e..9fe91219 100644 --- a/tools/mcc-debug.sh +++ b/tools/mcc-debug.sh @@ -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,6 +36,8 @@ 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 @@ -42,6 +47,8 @@ while [[ $# -gt 0 ]]; do -v|--version) VERSION="$2"; shift 2 ;; -m|--mode) MODE="$2"; shift 2 ;; -p|--port) PORT="$2"; PORT_SET_BY_USER=true; shift 2 ;; + --session) SESSION="$2"; shift 2 ;; + --username) USERNAME="$2"; shift 2 ;; --no-build) DO_BUILD=false; shift ;; --debug-on) DEBUG_ON=true; shift ;; --file-input) FILE_INPUT=true; shift ;; @@ -50,23 +57,38 @@ 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 + +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 " 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 @@ -82,7 +104,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 +152,55 @@ 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" < "$INPUT_FILE" rm -f "$MCC_LOG" +rm -f "$PID_FILE" -MCC_ARGS=("$CFG" "CursorBot" "-" "localhost:$PORT") +MCC_ARGS=("$CFG" "$USERNAME" "-" "localhost:$PORT") 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 \ + # 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' && dotnet run --project MinecraftClient -c Release --no-build -- ${MCC_ARGS[*]}; 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 + # FileInput mode: run in background, drive via session-specific input file ( cd "$REPO_ROOT" - MCC_FILE_INPUT=1 dotnet run --project MinecraftClient -c Release --no-build -- "${MCC_ARGS[@]}" > "$MCC_LOG" 2>&1 + MCC_FILE_INPUT=1 MCC_INPUT_FILE="$INPUT_FILE" dotnet run --project MinecraftClient -c Release --no-build -- "${MCC_ARGS[@]}" > "$MCC_LOG" 2>&1 ) & MCC_PID=$! + printf '%s\n' "$MCC_PID" > "$PID_FILE" echo " MCC PID: $MCC_PID" + echo " MCC PID file: $PID_FILE" echo "" sleep 5 @@ -175,25 +218,26 @@ 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 " Stop MCC: echo 'quit' >> $INPUT_FILE" echo " Stop server: mc-stop $VERSION" 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 \ + # 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' && dotnet run --project MinecraftClient -c Release --no-build -- ${MCC_ARGS[*]}; 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" diff --git a/tools/mcc-env.sh b/tools/mcc-env.sh index eb735894..61de29c4 100644 --- a/tools/mcc-env.sh +++ b/tools/mcc-env.sh @@ -189,7 +189,58 @@ mcc-cmd() { command="${command_parts[*]}" printf '%s\n' "$command" >> "$input_file" } -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-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 + 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 + kill "$pid" 2>/dev/null || true + echo "Killed MCC PID $pid for session '$session'" + killed=true + 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-reload() { mcc-kill sleep 1 diff --git a/tools/mcc-log-tail.sh b/tools/mcc-log-tail.sh index fb888a42..557c6b88 100644 --- a/tools/mcc-log-tail.sh +++ b/tools/mcc-log-tail.sh @@ -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,25 @@ 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 + --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 [--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 +44,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 From 0519eac4a8c6b84954c3c120e4548c80e065b5b5 Mon Sep 17 00:00:00 2001 From: BruceChen Date: Sun, 12 Apr 2026 19:47:36 +0800 Subject: [PATCH 11/18] tools: run file-input debug sessions in tmux --- tools/mcc-debug.sh | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/tools/mcc-debug.sh b/tools/mcc-debug.sh index 9fe91219..52e607c1 100644 --- a/tools/mcc-debug.sh +++ b/tools/mcc-debug.sh @@ -177,6 +177,7 @@ rm -f "$MCC_LOG" rm -f "$PID_FILE" 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 @@ -192,13 +193,28 @@ if [[ "$MODE" == "tui" ]]; then echo " Kill MCC: tmux kill-session -t $MCC_TMUX_SESSION" echo "" elif $FILE_INPUT; then - # FileInput mode: run in background, drive via session-specific input file - ( - cd "$REPO_ROOT" - MCC_FILE_INPUT=1 MCC_INPUT_FILE="$INPUT_FILE" dotnet run --project MinecraftClient -c Release --no-build -- "${MCC_ARGS[@]}" > "$MCC_LOG" 2>&1 - ) & - MCC_PID=$! - printf '%s\n' "$MCC_PID" > "$PID_FILE" + # 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 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 "" @@ -219,6 +235,7 @@ elif $FILE_INPUT; then 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 "" From e0752d8d98a26bef199716c623bdcfe7c3fa1cb8 Mon Sep 17 00:00:00 2001 From: BruceChen Date: Sun, 12 Apr 2026 19:57:54 +0800 Subject: [PATCH 12/18] tools: harden task3 session debug scripts --- tools/mcc-debug.sh | 50 +++++++++++++++++++++++++++++++++++++------ tools/mcc-env.sh | 14 ++++++++---- tools/mcc-log-tail.sh | 28 +++++++++++++++++++++--- 3 files changed, 78 insertions(+), 14 deletions(-) diff --git a/tools/mcc-debug.sh b/tools/mcc-debug.sh index 52e607c1..a96bfc7c 100644 --- a/tools/mcc-debug.sh +++ b/tools/mcc-debug.sh @@ -44,11 +44,47 @@ FILE_INPUT=false 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 ;; - --session) SESSION="$2"; shift 2 ;; - --username) USERNAME="$2"; 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 ;; @@ -183,7 +219,7 @@ if [[ "$MODE" == "tui" ]]; then # 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' && dotnet run --project MinecraftClient -c Release --no-build -- ${MCC_ARGS[*]}; echo '=== MCC EXITED ==='; sleep 600" + "cd '$REPO_ROOT' && 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_TMUX_SESSION'" echo " (TUI mode uses a real terminal; log file is not available, use MCC's /debug command)" @@ -243,7 +279,7 @@ else # 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' && dotnet run --project MinecraftClient -c Release --no-build -- ${MCC_ARGS[*]}; echo '=== MCC EXITED ==='; sleep 600" + "cd '$REPO_ROOT' && 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_TMUX_SESSION'" echo "" diff --git a/tools/mcc-env.sh b/tools/mcc-env.sh index 61de29c4..c848e586 100644 --- a/tools/mcc-env.sh +++ b/tools/mcc-env.sh @@ -210,7 +210,7 @@ mcc-kill() { done session="$(_mcc_resolve_session "$session")" - local pid_file meta_file tmux_session pid + 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")" @@ -219,9 +219,15 @@ mcc-kill() { if [[ -f "$pid_file" ]]; then pid="$(tr -cd '0-9' < "$pid_file")" if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then - kill "$pid" 2>/dev/null || true - echo "Killed MCC PID $pid for session '$session'" - killed=true + 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 diff --git a/tools/mcc-log-tail.sh b/tools/mcc-log-tail.sh index 557c6b88..2fc269bb 100644 --- a/tools/mcc-log-tail.sh +++ b/tools/mcc-log-tail.sh @@ -18,9 +18,31 @@ SERVER_ONLY=false 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 ;; + --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 [--session NAME] [--server VER] [--server-only VER]" exit 0 ;; From 0a041b37c5730b47fd2903fe550cdb5f35f40509 Mon Sep 17 00:00:00 2001 From: BruceChen Date: Sun, 12 Apr 2026 20:07:15 +0800 Subject: [PATCH 13/18] Add worktree-isolated tmpfs build outputs --- Directory.Build.props | 11 +++++++++++ tools/mcc-env.sh | 27 +++++++++++++++++++++++---- tools/test-mcc-env.sh | 4 ++++ 3 files changed, 38 insertions(+), 4 deletions(-) create mode 100644 Directory.Build.props diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 00000000..41d20762 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,11 @@ + + + $(DefaultItemExcludes);**/bin/**;**/obj/** + + + + $(MCC_BUILD_ROOT)/$(MSBuildProjectName)/bin/ + $(MCC_BUILD_ROOT)/$(MSBuildProjectName)/obj/ + $(BaseIntermediateOutputPath) + + diff --git a/tools/mcc-env.sh b/tools/mcc-env.sh index c848e586..170d9450 100644 --- a/tools/mcc-env.sh +++ b/tools/mcc-env.sh @@ -121,6 +121,20 @@ _mcc_build_root() { printf '%s\n' "$MCC_REPO_ROOT" } +_mcc_dotnet_env() { + local -n env_ref="$1" + env_ref=() + + if [[ "${MCC_BUILD_MODE:-local}" != "tmpfs" ]]; then + return 0 + fi + + local build_root + build_root="$(_mcc_build_root)" + mkdir -p "$build_root" + env_ref+=("MCC_BUILD_ROOT=$build_root") +} + # Helper: convert version to tmux session name (dots -> underscores) _mc-session() { echo "mc-${1//\./_}"; } @@ -141,15 +155,20 @@ mc-rcon() { bash "$MCC_REPO/tools/mc-rcon.sh" "$@"; } # --- MCC Build/Run --- mcc-build() { local repo_root + local -a dotnet_env repo_root="$(_mcc_repo_root)" + _mcc_dotnet_env dotnet_env + env "${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)" - 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 + rm -rf "$build_root" + return 0 fi + + dotnet clean "$(_mcc_repo_root)/MinecraftClient.sln" -c Release } mcc-run() { local port="${1:-25565}" diff --git a/tools/test-mcc-env.sh b/tools/test-mcc-env.sh index 2b8e1563..e4925f8e 100755 --- a/tools/test-mcc-env.sh +++ b/tools/test-mcc-env.sh @@ -75,6 +75,10 @@ if [[ "$build_root" != "$expected_prefix"* ]]; then exit 1 fi +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")" From e4d71d970b1212c7f6a5271d7cac036f8c8dde13 Mon Sep 17 00:00:00 2001 From: BruceChen Date: Sun, 12 Apr 2026 20:21:59 +0800 Subject: [PATCH 14/18] Make tmpfs dotnet env wrapper bash-3 compatible --- tools/mcc-env.sh | 35 ++++++++++++++++++++--------------- tools/test-mcc-env.sh | 3 +++ 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/tools/mcc-env.sh b/tools/mcc-env.sh index 170d9450..b3825529 100644 --- a/tools/mcc-env.sh +++ b/tools/mcc-env.sh @@ -122,17 +122,15 @@ _mcc_build_root() { } _mcc_dotnet_env() { - local -n env_ref="$1" - env_ref=() - - if [[ "${MCC_BUILD_MODE:-local}" != "tmpfs" ]]; then - return 0 + 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 - local build_root - build_root="$(_mcc_build_root)" - mkdir -p "$build_root" - env_ref+=("MCC_BUILD_ROOT=$build_root") + "$@" } # Helper: convert version to tmux session name (dots -> underscores) @@ -155,10 +153,8 @@ mc-rcon() { bash "$MCC_REPO/tools/mc-rcon.sh" "$@"; } # --- MCC Build/Run --- mcc-build() { local repo_root - local -a dotnet_env repo_root="$(_mcc_repo_root)" - _mcc_dotnet_env dotnet_env - env "${dotnet_env[@]}" dotnet build "$repo_root/MinecraftClient.sln" -c Release + _mcc_dotnet_env dotnet build "$repo_root/MinecraftClient.sln" -c Release } mcc-build-clean() { if [[ "${MCC_BUILD_MODE:-local}" == "tmpfs" ]]; then @@ -173,7 +169,7 @@ mcc-build-clean() { 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 + cd "$MCC_REPO" && _mcc_dotnet_env env MCC_FILE_INPUT=1 dotnet run --project MinecraftClient -c Release -- CursorBot - "localhost:${port}" "$@" 2>&1 } mcc-cmd() { local session="" @@ -275,9 +271,18 @@ mcc-reload() { # --- TUI Mode --- mcc-tui() { local port="${1:-25565}" + local cmd_prefix="" 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" + + if [[ "${MCC_BUILD_MODE:-local}" == "tmpfs" ]]; then + local build_root + build_root="$(_mcc_build_root)" + mkdir -p "$build_root" + cmd_prefix="MCC_BUILD_ROOT='$build_root' " + fi + + _mcc_dotnet_env tmux new-session -d -s mcc-debug -x 160 -y 50 \ + "cd '$MCC_REPO' && ${cmd_prefix}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" } diff --git a/tools/test-mcc-env.sh b/tools/test-mcc-env.sh index e4925f8e..2a6c8d5b 100755 --- a/tools/test-mcc-env.sh +++ b/tools/test-mcc-env.sh @@ -75,6 +75,9 @@ if [[ "$build_root" != "$expected_prefix"* ]]; then 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" ]] From b665ebfeac5cc4a1feecdf410bd2b2cba17c5ea0 Mon Sep 17 00:00:00 2001 From: BruceChen Date: Sun, 12 Apr 2026 20:46:39 +0800 Subject: [PATCH 15/18] Add session-scoped dual-session smoke test and harness updates --- .../scripts/reset_shared_test_state.sh | 6 +- .../scripts/run_full_spectrum_test.sh | 99 ++++----- .../run_parallel_session_smoke_test.sh | 197 ++++++++++++++++++ tools/run-creative-e2e.sh | 43 ++-- 4 files changed, 272 insertions(+), 73 deletions(-) create mode 100755 .skills/mcc-integration-testing/scripts/run_parallel_session_smoke_test.sh diff --git a/.skills/mcc-integration-testing/scripts/reset_shared_test_state.sh b/.skills/mcc-integration-testing/scripts/reset_shared_test_state.sh index 2d84ac1b..f3945e1a 100755 --- a/.skills/mcc-integration-testing/scripts/reset_shared_test_state.sh +++ b/.skills/mcc-integration-testing/scripts/reset_shared_test_state.sh @@ -12,7 +12,7 @@ usage() { cat <<'EOF' Usage: reset_shared_test_state.sh [--all | ...] -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 @@ -46,5 +44,3 @@ else remove_stale_stdin_pipe "$version" done fi - -rm -f "$MCC_REPO/mcc_input.txt" diff --git a/.skills/mcc-integration-testing/scripts/run_full_spectrum_test.sh b/.skills/mcc-integration-testing/scripts/run_full_spectrum_test.sh index 51021974..4cfb5ae9 100755 --- a/.skills/mcc-integration-testing/scripts/run_full_spectrum_test.sh +++ b/.skills/mcc-integration-testing/scripts/run_full_spectrum_test.sh @@ -17,23 +17,22 @@ 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="CursorBot" +MCC_LOG="$(_mcc_session_log_file "$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 wait_for_server_stop "$VERSION" 20 >/dev/null 2>&1 || true @@ -41,7 +40,7 @@ cleanup() { 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,19 +128,22 @@ run_server_command() { run_mcc_command() { local cmd="$1" echo "MCC> $cmd" - mcc-cmd "$cmd" + mcc-cmd --session "$SESSION_NAME" "$cmd" sleep 2 } 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")" +mkdir -p "$(dirname "$INPUT_FILE")" "$(dirname "$MCC_LOG")" : > "$INPUT_FILE" +rm -f "$MCC_LOG" echo "Starting server..." mc-start "$VERSION" >/dev/null @@ -150,14 +152,15 @@ 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_FILE_INPUT=1 MCC_INPUT_FILE="$INPUT_FILE" \ + dotnet run --project MinecraftClient -c Release --no-build -- "$CFG" "$TEST_USERNAME" - "localhost:$SERVER_PORT" > "$MCC_LOG" 2>&1 ) & MCC_PID=$! wait_for_file_pattern "$MCC_LOG" "Server was successfully joined." "MCC join success" 90 || fail "MCC failed to join" -wait_for_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 +179,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 +196,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 +236,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 +298,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 <&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" >/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" + bash "$REPO_ROOT/tools/mcc-debug.sh" \ + --version "$VERSION" \ + --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" +echo "Starting MCC session B..." +start_file_input_session "$SESSION_B" "$USERNAME_B" + +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 </dev/null; then - echo "quit" >> "$INPUT_FILE" 2>/dev/null || true + mcc-cmd --session "$MCC_SESSION" "quit" >/dev/null 2>&1 || true sleep 2 - kill "$MCC_PID" 2>/dev/null || true - wait "$MCC_PID" 2>/dev/null || true fi + 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 @@ -100,7 +101,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 +116,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" } @@ -151,9 +152,9 @@ 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 +162,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 +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 @@ -201,9 +204,9 @@ prepare_config ( 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:$SERVER_PORT" \ > "$MCC_LOG" 2>&1 @@ -211,10 +214,10 @@ prepare_config MCC_PID=$! 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 +240,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" From af285866dc6fce79010a57f72a2abf878e0ae9a5 Mon Sep 17 00:00:00 2001 From: BruceChen Date: Sun, 12 Apr 2026 23:33:45 +0800 Subject: [PATCH 16/18] tools: harden shared server session workflow --- .../mcc-integration-testing/scripts/common.sh | 4 +- .../scripts/ensure_offline_server.sh | 4 +- .../scripts/reset_shared_test_state.sh | 4 +- .../scripts/run_achievements_test.sh | 2 +- .../scripts/run_full_spectrum_test.sh | 37 +++- .../run_parallel_session_smoke_test.sh | 8 +- tools/mcc-debug.sh | 20 +- tools/mcc-env.sh | 202 ++++++++++++++++-- tools/run-creative-e2e.sh | 81 +++++-- tools/test-mcc-env.sh | 33 ++- 10 files changed, 331 insertions(+), 64 deletions(-) diff --git a/.skills/mcc-integration-testing/scripts/common.sh b/.skills/mcc-integration-testing/scripts/common.sh index 973b5da3..ac5c0250 100755 --- a/.skills/mcc-integration-testing/scripts/common.sh +++ b/.skills/mcc-integration-testing/scripts/common.sh @@ -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 } diff --git a/.skills/mcc-integration-testing/scripts/ensure_offline_server.sh b/.skills/mcc-integration-testing/scripts/ensure_offline_server.sh index 38e73978..8ede375b 100755 --- a/.skills/mcc-integration-testing/scripts/ensure_offline_server.sh +++ b/.skills/mcc-integration-testing/scripts/ensure_offline_server.sh @@ -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 diff --git a/.skills/mcc-integration-testing/scripts/reset_shared_test_state.sh b/.skills/mcc-integration-testing/scripts/reset_shared_test_state.sh index f3945e1a..eab7c694 100755 --- a/.skills/mcc-integration-testing/scripts/reset_shared_test_state.sh +++ b/.skills/mcc-integration-testing/scripts/reset_shared_test_state.sh @@ -34,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 diff --git a/.skills/mcc-integration-testing/scripts/run_achievements_test.sh b/.skills/mcc-integration-testing/scripts/run_achievements_test.sh index df0236e0..4ea0c3ec 100755 --- a/.skills/mcc-integration-testing/scripts/run_achievements_test.sh +++ b/.skills/mcc-integration-testing/scripts/run_achievements_test.sh @@ -124,7 +124,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 diff --git a/.skills/mcc-integration-testing/scripts/run_full_spectrum_test.sh b/.skills/mcc-integration-testing/scripts/run_full_spectrum_test.sh index 4cfb5ae9..181862a8 100755 --- a/.skills/mcc-integration-testing/scripts/run_full_spectrum_test.sh +++ b/.skills/mcc-integration-testing/scripts/run_full_spectrum_test.sh @@ -18,14 +18,15 @@ RUN_ID="$(date +%Y%m%d-%H%M%S)" RUN_DIR="$RUN_ROOT/$RUN_ID" SERVER_LOG_FILE="$MCC_SERVERS/$VERSION/logs/latest.log" SESSION_NAME="full-spectrum-${MC_VERSION//[^a-zA-Z0-9]/_}" -TEST_USERNAME="CursorBot" +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="$(_mcc_session_input_file "$SESSION_NAME")" CFG="$RUN_DIR/MinecraftClient.$MC_VERSION.ini" -MCC_PID="" mkdir -p "$RUN_DIR" @@ -34,7 +35,7 @@ cleanup() { 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 @@ -132,6 +133,26 @@ run_mcc_command() { 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" @@ -140,6 +161,9 @@ 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" @@ -150,12 +174,7 @@ 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 MCC_INPUT_FILE="$INPUT_FILE" \ - dotnet run --project MinecraftClient -c Release --no-build -- "$CFG" "$TEST_USERNAME" - "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 "$TEST_USERNAME joined the game" "server join entry" 30 || fail "Server never logged the join" diff --git a/.skills/mcc-integration-testing/scripts/run_parallel_session_smoke_test.sh b/.skills/mcc-integration-testing/scripts/run_parallel_session_smoke_test.sh index 86535fe9..9f422d28 100755 --- a/.skills/mcc-integration-testing/scripts/run_parallel_session_smoke_test.sh +++ b/.skills/mcc-integration-testing/scripts/run_parallel_session_smoke_test.sh @@ -93,7 +93,7 @@ cleanup() { 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" >/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 @@ -119,8 +119,10 @@ assert_server_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" \ @@ -145,9 +147,9 @@ if [[ -z "$SERVER_PORT" ]]; then fi echo "Starting MCC session A..." -start_file_input_session "$SESSION_A" "$USERNAME_A" +start_file_input_session "$SESSION_A" "$USERNAME_A" "$SERVER_PORT" echo "Starting MCC session B..." -start_file_input_session "$SESSION_B" "$USERNAME_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" diff --git a/tools/mcc-debug.sh b/tools/mcc-debug.sh index a96bfc7c..d0ca0e78 100644 --- a/tools/mcc-debug.sh +++ b/tools/mcc-debug.sh @@ -41,6 +41,8 @@ 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 @@ -98,6 +100,12 @@ 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")" @@ -118,6 +126,7 @@ 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" @@ -132,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)" @@ -219,7 +228,7 @@ if [[ "$MODE" == "tui" ]]; then # 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' && dotnet run --project MinecraftClient -c Release --no-build -- $MCC_ARGS_CMD; echo '=== MCC EXITED ==='; sleep 600" + "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_TMUX_SESSION'" echo " (TUI mode uses a real terminal; log file is not available, use MCC's /debug command)" @@ -232,7 +241,7 @@ elif $FILE_INPUT; then # 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 MCC_FILE_INPUT=1 MCC_INPUT_FILE='$INPUT_FILE' dotnet run --project MinecraftClient -c Release --no-build -- $MCC_ARGS_CMD > '$MCC_LOG' 2>&1" + "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 @@ -274,12 +283,13 @@ elif $FILE_INPUT; then 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_TMUX_SESSION" 2>/dev/null || true tmux new-session -d -s "$MCC_TMUX_SESSION" -x 160 -y 50 \ - "cd '$REPO_ROOT' && dotnet run --project MinecraftClient -c Release --no-build -- $MCC_ARGS_CMD; echo '=== MCC EXITED ==='; sleep 600" + "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_TMUX_SESSION'" echo "" @@ -293,4 +303,4 @@ fi echo "Quick commands:" 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" diff --git a/tools/mcc-env.sh b/tools/mcc-env.sh index b3825529..217f18cc 100644 --- a/tools/mcc-env.sh +++ b/tools/mcc-env.sh @@ -136,16 +136,108 @@ _mcc_dotnet_env() { # 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 < "$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" "$@"; } @@ -167,9 +259,52 @@ mcc-build-clean() { dotnet clean "$(_mcc_repo_root)/MinecraftClient.sln" -c Release } mcc-run() { - local port="${1:-25565}" - shift || true - cd "$MCC_REPO" && _mcc_dotnet_env env MCC_FILE_INPUT=1 dotnet run --project MinecraftClient -c Release -- CursorBot - "localhost:${port}" "$@" 2>&1 + 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="" @@ -270,21 +405,52 @@ mcc-reload() { # --- TUI Mode --- mcc-tui() { - local port="${1:-25565}" - local cmd_prefix="" - shift || true + 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 - if [[ "${MCC_BUILD_MODE:-local}" == "tmpfs" ]]; then - local build_root - build_root="$(_mcc_build_root)" - mkdir -p "$build_root" - cmd_prefix="MCC_BUILD_ROOT='$build_root' " + 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 - _mcc_dotnet_env tmux new-session -d -s mcc-debug -x 160 -y 50 \ - "cd '$MCC_REPO' && ${cmd_prefix}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" + bash "$MCC_REPO/tools/mcc-debug.sh" "${args[@]}" } _mcc_session_log_tail() { diff --git a/tools/run-creative-e2e.sh b/tools/run-creative-e2e.sh index b264c9e9..ea25a464 100644 --- a/tools/run-creative-e2e.sh +++ b/tools/run-creative-e2e.sh @@ -37,11 +37,12 @@ SESSION_NAME="mc-${SERVER_DIR//./_}" TEST_ROOT="${TMPDIR:-/tmp}/mcc-creative-e2e/${SERVER_DIR//\//_}" CFG="$TEST_ROOT/MinecraftClient.$MC_VERSION.ini" MCC_SESSION="creative-e2e-${SERVER_DIR//[^a-zA-Z0-9]/_}-${PROFILE}" -TEST_USERNAME="CursorBot" +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="$(_mcc_session_input_file "$MCC_SESSION")" -MCC_PID="" SERVER_PORT="25565" mkdir -p "$TEST_ROOT" @@ -65,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 @@ -81,17 +111,16 @@ wait_for_rcon_port_free() { return 1 } cleanup() { - if [[ -n "${MCC_PID:-}" ]] && kill -0 "$MCC_PID" 2>/dev/null; then - mcc-cmd --session "$MCC_SESSION" "quit" >/dev/null 2>&1 || true - sleep 2 - 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 } @@ -148,6 +177,27 @@ 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" @@ -184,7 +234,7 @@ modern_mob_and_effects() { } 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")" @@ -202,16 +252,7 @@ prepare_config : > "$INPUT_FILE" -( - cd "$REPO_ROOT" - MCC_FILE_INPUT=1 MCC_INPUT_FILE="$INPUT_FILE" dotnet run --project MinecraftClient -c Release --no-build -- \ - "$CFG" \ - "$TEST_USERNAME" \ - - \ - "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" "$TEST_USERNAME joined the game" "server join entry" 30 diff --git a/tools/test-mcc-env.sh b/tools/test-mcc-env.sh index 2a6c8d5b..38c50c84 100755 --- a/tools/test-mcc-env.sh +++ b/tools/test-mcc-env.sh @@ -93,7 +93,7 @@ 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; do +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=$? @@ -106,4 +106,35 @@ for func in mcc-cmd mcc-reset-session mcc-state mcc-log-mcc; do 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" From ca5c9f73e3d9ab2885158d36bff673e9f95b2249 Mon Sep 17 00:00:00 2001 From: BruceChen Date: Sun, 12 Apr 2026 23:33:59 +0800 Subject: [PATCH 17/18] docs: describe shared server MCC sessions --- .skills/mcc-dev-workflow/SKILL.md | 128 ++- docs/guide/ai-assisted-development.md | 93 ++- ...-12-mcc-shared-server-isolated-sessions.md | 789 ++++++++++++++++++ tools/README.md | 32 + 4 files changed, 983 insertions(+), 59 deletions(-) create mode 100644 docs/superpowers/plans/2026-04-12-mcc-shared-server-isolated-sessions.md diff --git a/.skills/mcc-dev-workflow/SKILL.md b/.skills/mcc-dev-workflow/SKILL.md index b1a9ef01..79980b63 100644 --- a/.skills/mcc-dev-workflow/SKILL.md +++ b/.skills/mcc-dev-workflow/SKILL.md @@ -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:-/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//`. +- `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//` on Linux, or `${TMPDIR:-/tmp}/mcc-build//` 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//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-` +- **Logs**: `mcc-log-mcc --session smoke-a` or `tail -f "${TMPDIR:-/tmp}/mcc-debug//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- -p -S -50` captures the last 50 lines of a tmux session without attaching. ## Tool files diff --git a/docs/guide/ai-assisted-development.md b/docs/guide/ai-assisted-development.md index 9f8fdc03..852df5db 100644 --- a/docs/guide/ai-assisted-development.md +++ b/docs/guide/ai-assisted-development.md @@ -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` @@ -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 ``` @@ -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//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//`. +- `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//` on Linux, or `${TMPDIR:-/tmp}/mcc-build//` 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: diff --git a/docs/superpowers/plans/2026-04-12-mcc-shared-server-isolated-sessions.md b/docs/superpowers/plans/2026-04-12-mcc-shared-server-isolated-sessions.md new file mode 100644 index 00000000..975787b9 --- /dev/null +++ b/docs/superpowers/plans/2026-04-12-mcc-shared-server-isolated-sessions.md @@ -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] " >&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" <&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 + + + $(DefaultItemExcludes);**/bin/**;**/obj/** + + + + $(MCC_BUILD_ROOT)/$(MSBuildProjectName)/bin/ + $(MCC_BUILD_ROOT)/$(MSBuildProjectName)/obj/ + $(BaseIntermediateOutputPath) + + +``` + +- [ ] **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//` 或 `/tmp/mcc-build//` + +- [ ] **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//` on Linux, or `${TMPDIR:-/tmp}/mcc-build//` 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` 外置重复编译问题 diff --git a/tools/README.md b/tools/README.md index 7d33d2cf..5c9d29cd 100644 --- a/tools/README.md +++ b/tools/README.md @@ -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//` on Linux, or `${TMPDIR:-/tmp}/mcc-build//` when `/dev/shm` is unavailable. + ## Data Sources Two types of data can be used as input: From 8e0db1402f839850bdfe05ad35040d51215a8fbd Mon Sep 17 00:00:00 2001 From: BruceChen Date: Sun, 12 Apr 2026 23:43:54 +0800 Subject: [PATCH 18/18] chore: remove remaining CursorBot references --- .../references/command-matrix.md | 68 +++++++++---------- .../scripts/prepare_offline_mcc_config.sh | 4 +- .../scripts/run_achievements_test.sh | 23 ++++--- .../scripts/summarize_test_run.sh | 2 +- DebugTools/MccMcpSampleClient/Program.cs | 2 +- 5 files changed, 51 insertions(+), 48 deletions(-) diff --git a/.skills/mcc-integration-testing/references/command-matrix.md b/.skills/mcc-integration-testing/references/command-matrix.md index 31077374..782bc745 100644 --- a/.skills/mcc-integration-testing/references/command-matrix.md +++ b/.skills/mcc-integration-testing/references/command-matrix.md @@ -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) diff --git a/.skills/mcc-integration-testing/scripts/prepare_offline_mcc_config.sh b/.skills/mcc-integration-testing/scripts/prepare_offline_mcc_config.sh index 64727a58..1e30f673 100644 --- a/.skills/mcc-integration-testing/scripts/prepare_offline_mcc_config.sh +++ b/.skills/mcc-integration-testing/scripts/prepare_offline_mcc_config.sh @@ -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}" diff --git a/.skills/mcc-integration-testing/scripts/run_achievements_test.sh b/.skills/mcc-integration-testing/scripts/run_achievements_test.sh index 4ea0c3ec..1484acd6 100755 --- a/.skills/mcc-integration-testing/scripts/run_achievements_test.sh +++ b/.skills/mcc-integration-testing/scripts/run_achievements_test.sh @@ -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 @@ -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" diff --git a/.skills/mcc-integration-testing/scripts/summarize_test_run.sh b/.skills/mcc-integration-testing/scripts/summarize_test_run.sh index d0b74ff7..ac8efacf 100755 --- a/.skills/mcc-integration-testing/scripts/summarize_test_run.sh +++ b/.skills/mcc-integration-testing/scripts/summarize_test_run.sh @@ -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 diff --git a/DebugTools/MccMcpSampleClient/Program.cs b/DebugTools/MccMcpSampleClient/Program.cs index 0bd91fdb..15ac2b70 100644 --- a/DebugTools/MccMcpSampleClient/Program.cs +++ b/DebugTools/MccMcpSampleClient/Program.cs @@ -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;