From fd7a98b2638221c7f4daf70254ae93b9a02f3319 Mon Sep 17 00:00:00 2001 From: shokollm <270575765+shokollm@users.noreply.github.com> Date: Sun, 5 Apr 2026 12:10:55 +0000 Subject: [PATCH] fix: validate sessions in cmd_status + use isolated test environment 1. cmd_status now validates session IDs against opencode session list - Reports 'error: base session X not found in opencode' if missing - Reports 'error: pm_agent session X not found in opencode' if missing 2. Test suite now uses isolated KUGETSU_DIR=/tmp/test-kugetsu-$$ - All tests use separate test directory instead of ~/.kugetsu - Prevents test suite from corrupting real user data - Cleanup removes test directory entirely Fixes #148 --- CONTRIBUTING.md | 9 +- skills/kugetsu/scripts/kugetsu | 12 +++ skills/kugetsu/tests/test-kugetsu-v2.sh | 108 ++++++++++++------------ 3 files changed, 73 insertions(+), 56 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ae2f1e8..0b69f4a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,10 +2,10 @@ ## Workflow -1. Create a branch for your work: `git checkout -b fix/issue-N-name` or `git checkout -b docs/topic-name` +1. Create a branch for your work: `git checkout -b fix/issue-N-name` or `git checkout -b feat/issue-N-feature-name` 2. Make changes and commit with clear messages 3. Open a Pull Request for review -4. Do not merge directly to `master` for reviewable changes +4. Do not merge directly to `main` for reviewable changes 5. After approval, squash and merge ## Guidelines @@ -17,7 +17,10 @@ ## Branches -- `master` — stable, reviewed content only +- `main` — stable, reviewed content only +- `develop` — experimental work for 0.2.x - `fix/*` — bug fixes +- `feat/*` — new features - `docs/*` — documentation updates +- `refactor/*` — refactoring - `research/*` — new research notes diff --git a/skills/kugetsu/scripts/kugetsu b/skills/kugetsu/scripts/kugetsu index 5095955..bd0ba76 100755 --- a/skills/kugetsu/scripts/kugetsu +++ b/skills/kugetsu/scripts/kugetsu @@ -1075,6 +1075,18 @@ cmd_status() { return fi + local opencode_sessions=$(opencode session list 2>/dev/null | grep -oP '^ses_\w+' || true) + + if ! echo "$opencode_sessions" | grep -q "^${base}$"; then + echo "error: base session '$base' not found in opencode" + return + fi + + if ! echo "$opencode_sessions" | grep -q "^${pm_agent}$"; then + echo "error: pm_agent session '$pm_agent' not found in opencode" + return + fi + echo "ok" } diff --git a/skills/kugetsu/tests/test-kugetsu-v2.sh b/skills/kugetsu/tests/test-kugetsu-v2.sh index 70835a7..4675436 100644 --- a/skills/kugetsu/tests/test-kugetsu-v2.sh +++ b/skills/kugetsu/tests/test-kugetsu-v2.sh @@ -7,6 +7,8 @@ set -euo pipefail KUGETSU="./skills/kugetsu/scripts/kugetsu" +TEST_KUGETSU_DIR="/tmp/test-kugetsu-$$" +export KUGETSU_DIR="$TEST_KUGETSU_DIR" TEST_ISSUE_REF="github.com/shoko/kugetsu#14" TEST_DISCUSS_REF="github.com/shoko/kugetsu#-discuss" TEST_BASE_SESSION_ID="ses_test_base_123" @@ -18,28 +20,28 @@ PASS=0 FAIL=0 cleanup() { - rm -rf ~/.kugetsu/sessions/* ~/.kugetsu/worktrees/* ~/.kugetsu/index.json 2>/dev/null || true + rm -rf "$TEST_KUGETSU_DIR" 2>/dev/null || true } setup_mock_base() { - mkdir -p ~/.kugetsu/sessions ~/.kugetsu/worktrees - cat > ~/.kugetsu/index.json << EOF + mkdir -p "$TEST_KUGETSU_DIR/sessions" "$TEST_KUGETSU_DIR/worktrees" + cat > "$TEST_KUGETSU_DIR/index.json" << EOF { "base": "$TEST_BASE_SESSION_ID", "pm_agent": "$TEST_PM_AGENT_SESSION_ID", "issues": {} } EOF - cat > ~/.kugetsu/sessions/$TEST_BASE_SESSION_FILE << EOF + cat > "$TEST_KUGETSU_DIR/sessions/$TEST_BASE_SESSION_FILE" << EOF {"type": "base", "opencode_session_id": "$TEST_BASE_SESSION_ID", "created_at": "2026-03-29T18:00:00+02:00", "state": "idle"} EOF - cat > ~/.kugetsu/sessions/$TEST_PM_AGENT_SESSION_FILE << EOF + cat > "$TEST_KUGETSU_DIR/sessions/$TEST_PM_AGENT_SESSION_FILE" << EOF {"type": "pm_agent", "opencode_session_id": "$TEST_PM_AGENT_SESSION_ID", "created_at": "2026-03-29T18:00:00+02:00", "state": "idle"} EOF } setup_mock_forked() { - cat > ~/.kugetsu/index.json << EOF + cat > "$TEST_KUGETSU_DIR/index.json" << EOF { "base": "$TEST_BASE_SESSION_ID", "pm_agent": "$TEST_PM_AGENT_SESSION_ID", @@ -48,7 +50,7 @@ setup_mock_forked() { } } EOF - cat > ~/.kugetsu/sessions/$TEST_FORKED_SESSION_FILE << EOF + cat > "$TEST_KUGETSU_DIR/sessions/$TEST_FORKED_SESSION_FILE" << EOF {"type": "forked", "issue_ref": "$TEST_ISSUE_REF", "opencode_session_id": "ses_forked_789", "worktree_path": "/tmp/test-worktree", "created_at": "2026-03-29T18:00:00+02:00", "state": "idle"} EOF } @@ -112,16 +114,16 @@ echo "" # Test 3b: start fails without pm-agent echo "--- Test: start without pm-agent session ---" -rm -f ~/.kugetsu/index.json ~/.kugetsu/sessions/* -mkdir -p ~/.kugetsu/sessions -cat > ~/.kugetsu/index.json << EOF +rm -f $TEST_KUGETSU_DIR/index.json $TEST_KUGETSU_DIR/sessions/* +mkdir -p $TEST_KUGETSU_DIR/sessions +cat > $TEST_KUGETSU_DIR/index.json << EOF { "base": "$TEST_BASE_SESSION_ID", "pm_agent": null, "issues": {} } EOF -cat > ~/.kugetsu/sessions/$TEST_BASE_SESSION_FILE << EOF +cat > $TEST_KUGETSU_DIR/sessions/$TEST_BASE_SESSION_FILE << EOF {"type": "base", "opencode_session_id": "$TEST_BASE_SESSION_ID", "created_at": "2026-03-29T18:00:00+02:00", "state": "idle"} EOF OUTPUT=$($KUGETSU start github.com/shoko/kugetsu#14 "test" 2>&1 || true) @@ -176,7 +178,7 @@ echo "" # Test 6c: index.json has pm_agent field echo "--- Test: index.json has pm_agent field ---" -if grep -q '"pm_agent"' ~/.kugetsu/index.json; then +if grep -q '"pm_agent"' $TEST_KUGETSU_DIR/index.json; then pass "index.json has pm_agent field" else fail "index.json missing pm_agent field" @@ -227,12 +229,12 @@ echo "" echo "--- Test: destroy --pm-agent -y ---" setup_mock_base OUTPUT=$($KUGETSU destroy --pm-agent -y 2>&1 || true) -if [ -f ~/.kugetsu/sessions/$TEST_PM_AGENT_SESSION_FILE ]; then +if [ -f $TEST_KUGETSU_DIR/sessions/$TEST_PM_AGENT_SESSION_FILE ]; then fail "destroy --pm-agent -y removes pm-agent file" else pass "destroy --pm-agent -y removes pm-agent file" fi -if grep -q '"pm_agent": null' ~/.kugetsu/index.json; then +if grep -q '"pm_agent": null' $TEST_KUGETSU_DIR/index.json; then pass "destroy --pm-agent -y sets pm_agent to null in index" else fail "destroy --pm-agent -y should set pm_agent to null" @@ -243,7 +245,7 @@ echo "" echo "--- Test: destroy --base -y ---" setup_mock_base OUTPUT=$($KUGETSU destroy --base -y 2>&1 || true) -if [ -f ~/.kugetsu/sessions/$TEST_BASE_SESSION_FILE ]; then +if [ -f $TEST_KUGETSU_DIR/sessions/$TEST_BASE_SESSION_FILE ]; then fail "destroy --base -y removes base file" else pass "destroy --base -y removes base file" @@ -292,7 +294,7 @@ echo "" # Test 15: worktree path in session file echo "--- Test: worktree_path in session file ---" -if grep -q "worktree_path" ~/.kugetsu/sessions/$TEST_FORKED_SESSION_FILE; then +if grep -q "worktree_path" $TEST_KUGETSU_DIR/sessions/$TEST_FORKED_SESSION_FILE; then pass "session file contains worktree_path" else fail "session file missing worktree_path" @@ -303,7 +305,7 @@ echo "" echo "--- Test: prune with orphaned worktree ---" cleanup setup_mock_base -mkdir -p ~/.kugetsu/worktrees/orphaned-worktree +mkdir -p $TEST_KUGETSU_DIR/worktrees/orphaned-worktree OUTPUT=$($KUGETSU prune 2>&1 || true) if echo "$OUTPUT" | grep -q "orphaned worktree"; then pass "prune detects orphaned worktree" @@ -315,7 +317,7 @@ echo "" # Test 17: prune --force removes orphaned worktrees echo "--- Test: prune --force removes orphaned worktrees ---" OUTPUT=$($KUGETSU prune --force 2>&1 || true) -if [ -d ~/.kugetsu/worktrees/orphaned-worktree ]; then +if [ -d $TEST_KUGETSU_DIR/worktrees/orphaned-worktree ]; then fail "prune --force should remove orphaned worktree" else pass "prune --force removes orphaned worktree" @@ -332,10 +334,10 @@ echo "" echo "--- Test: destroy removes worktree ---" cleanup setup_mock_forked -# remove_worktree_for_issue derives path from issue ref: ~/.kugetsu/worktrees/github.com-shoko-kugetsu-14 -mkdir -p ~/.kugetsu/worktrees/github.com-shoko-kugetsu-14 +# remove_worktree_for_issue derives path from issue ref: $TEST_KUGETSU_DIR/worktrees/github.com-shoko-kugetsu-14 +mkdir -p $TEST_KUGETSU_DIR/worktrees/github.com-shoko-kugetsu-14 OUTPUT=$($KUGETSU destroy github.com/shoko/kugetsu#14 -y 2>&1 || true) -if [ -d ~/.kugetsu/worktrees/github.com-shoko-kugetsu-14 ]; then +if [ -d $TEST_KUGETSU_DIR/worktrees/github.com-shoko-kugetsu-14 ]; then fail "destroy should remove worktree" else pass "destroy removes worktree" @@ -345,7 +347,7 @@ echo "" # Test 20: session file properly formatted for v2.2 echo "--- Test: session file format v2.2 ---" setup_mock_forked -SESSION_CONTENT=$(cat ~/.kugetsu/sessions/$TEST_FORKED_SESSION_FILE) +SESSION_CONTENT=$(cat $TEST_KUGETSU_DIR/sessions/$TEST_FORKED_SESSION_FILE) if echo "$SESSION_CONTENT" | grep -q '"type": "forked"' && \ echo "$SESSION_CONTENT" | grep -q '"worktree_path"'; then pass "session file has v2.2 format" @@ -367,8 +369,8 @@ echo "" # Test 22: status when base missing echo "--- Test: status (base missing) ---" -mkdir -p ~/.kugetsu/sessions -cat > ~/.kugetsu/index.json << EOF +mkdir -p $TEST_KUGETSU_DIR/sessions +cat > $TEST_KUGETSU_DIR/index.json << EOF { "base": null, "pm_agent": "$TEST_PM_AGENT_SESSION_ID", @@ -385,7 +387,7 @@ echo "" # Test 23: status when pm-agent missing echo "--- Test: status (pm-agent missing) ---" -cat > ~/.kugetsu/index.json << EOF +cat > $TEST_KUGETSU_DIR/index.json << EOF { "base": "$TEST_BASE_SESSION_ID", "pm_agent": null, @@ -402,7 +404,7 @@ echo "" # Test 24: status when pm-agent is "None" (Python None output) echo "--- Test: status (pm-agent is Python None) ---" -cat > ~/.kugetsu/index.json << EOF +cat > $TEST_KUGETSU_DIR/index.json << EOF { "base": "$TEST_BASE_SESSION_ID", "pm_agent": "None", @@ -445,8 +447,8 @@ echo "" # Test 27: delegate when pm-agent missing echo "--- Test: delegate (pm-agent missing) ---" cleanup -mkdir -p ~/.kugetsu/sessions ~/.kugetsu/worktrees -cat > ~/.kugetsu/index.json << EOF +mkdir -p $TEST_KUGETSU_DIR/sessions $TEST_KUGETSU_DIR/worktrees +cat > $TEST_KUGETSU_DIR/index.json << EOF { "base": "$TEST_BASE_SESSION_ID", "pm_agent": null, @@ -508,7 +510,7 @@ echo "" # Test 32: delegate is fire-and-forget (returns immediately) echo "--- Test: delegate is fire-and-forget ---" setup_mock_base -mkdir -p ~/.kugetsu/logs +mkdir -p $TEST_KUGETSU_DIR/logs START=$(date +%s) OUTPUT=$($KUGETSU delegate "test fire-and-forget" 2>&1 || true) END=$(date +%s) @@ -527,10 +529,10 @@ echo "" # Test 33: delegate creates log file echo "--- Test: delegate creates log file ---" setup_mock_base -LOG_COUNT_BEFORE=$(ls ~/.kugetsu/logs/*.log 2>/dev/null | wc -l) +LOG_COUNT_BEFORE=$(ls $TEST_KUGETSU_DIR/logs/*.log 2>/dev/null | wc -l) $KUGETSU delegate "test log file" 2>&1 || true sleep 1 -LOG_COUNT_AFTER=$(ls ~/.kugetsu/logs/*.log 2>/dev/null | wc -l) +LOG_COUNT_AFTER=$(ls $TEST_KUGETSU_DIR/logs/*.log 2>/dev/null | wc -l) if [ $LOG_COUNT_AFTER -gt $LOG_COUNT_BEFORE ]; then pass "delegate creates log file" else @@ -558,10 +560,10 @@ echo "" # Test E2: env set creates file echo "--- Test: env set creates env file ---" -mkdir -p ~/.kugetsu/env -rm -f ~/.kugetsu/env/pm-agent.env +mkdir -p $TEST_KUGETSU_DIR/env +rm -f $TEST_KUGETSU_DIR/env/pm-agent.env $KUGETSU env set TEST_VAR "test_value" pm-agent 2>&1 || true -if [ -f ~/.kugetsu/env/pm-agent.env ]; then +if [ -f $TEST_KUGETSU_DIR/env/pm-agent.env ]; then pass "env set creates pm-agent.env file" else fail "env set did not create pm-agent.env" @@ -570,7 +572,7 @@ echo "" # Test E3: env show masks sensitive values echo "--- Test: env show masks sensitive values ---" -cat > ~/.kugetsu/env/pm-agent.env << 'ENVEOF' +cat > $TEST_KUGETSU_DIR/env/pm-agent.env << 'ENVEOF' export GITEA_TOKEN="secret_token_123" export MY_VAR="visible_value" ENVEOF @@ -584,14 +586,14 @@ echo "" # Test E4: Variables exported to child processes via set -a echo "--- Test: set -a exports variables to children ---" -mkdir -p ~/.kugetsu/env -cat > ~/.kugetsu/env/test.env << 'ENVEOF' +mkdir -p $TEST_KUGETSU_DIR/env +cat > $TEST_KUGETSU_DIR/env/test.env << 'ENVEOF' export EXPORT_TEST="exported_value" SIMPLE_TEST="not_exported" ENVEOF # Simulate what cmd_delegate does -ENV_FILE="~/.kugetsu/env/test.env" +ENV_FILE="$TEST_KUGETSU_DIR/env/test.env" env_sh="set -a; source '$ENV_FILE'; set +a; " result=$(bash -c "${env_sh}bash -c 'echo \$EXPORT_TEST'") @@ -604,11 +606,11 @@ echo "" # Test E5: pm-agent.env takes precedence echo "--- Test: pm-agent.env takes precedence over default ---" -mkdir -p ~/.kugetsu/env -cat > ~/.kugetsu/env/default.env << 'ENVEOF' +mkdir -p $TEST_KUGETSU_DIR/env +cat > $TEST_KUGETSU_DIR/env/default.env << 'ENVEOF' export GITEA_TOKEN="default_token" ENVEOF -cat > ~/.kugetsu/env/pm-agent.env << 'ENVEOF' +cat > $TEST_KUGETSU_DIR/env/pm-agent.env << 'ENVEOF' export GITEA_TOKEN="pm_agent_token" ENVEOF @@ -644,7 +646,7 @@ fi echo "" # Cleanup env files -rm -rf ~/.kugetsu/env 2>/dev/null || true +rm -rf $TEST_KUGETSU_DIR/env 2>/dev/null || true # Test E7: fix_session_permissions function exists echo "--- Test: fix_session_permissions function exists ---" @@ -736,7 +738,7 @@ PASS=0 FAIL=0 test_cleanup() { - rm -rf ~/.kugetsu/sessions/* ~/.kugetsu/worktrees/* ~/.kugetsu/index.json ~/.kugetsu/logs/* ~/.kugetsu/.agent_count ~/.kugetsu/.agent_lock 2>/dev/null || true + rm -rf $TEST_KUGETSU_DIR/sessions/* $TEST_KUGETSU_DIR/worktrees/* $TEST_KUGETSU_DIR/index.json $TEST_KUGETSU_DIR/logs/* $TEST_KUGETSU_DIR/.agent_count $TEST_KUGETSU_DIR/.agent_lock 2>/dev/null || true } pass() { @@ -750,25 +752,25 @@ fail() { } setup_mock_sessions() { - mkdir -p ~/.kugetsu/sessions ~/.kugetsu/worktrees ~/.kugetsu/logs - cat > ~/.kugetsu/index.json << INDEX + mkdir -p $TEST_KUGETSU_DIR/sessions $TEST_KUGETSU_DIR/worktrees $TEST_KUGETSU_DIR/logs + cat > $TEST_KUGETSU_DIR/index.json << INDEX { "base": "ses_test_base_123", "pm_agent": "ses_test_pm_456", "issues": {} } INDEX - echo '{"type": "base", "opencode_session_id": "ses_test_base_123", "created_at": "2026-03-29T18:00:00+02:00", "state": "idle"}' > ~/.kugetsu/sessions/base.json - echo '{"type": "pm_agent", "opencode_session_id": "ses_test_pm_456", "created_at": "2026-03-29T18:00:00+02:00", "state": "idle"}' > ~/.kugetsu/sessions/pm-agent.json + echo '{"type": "base", "opencode_session_id": "ses_test_base_123", "created_at": "2026-03-29T18:00:00+02:00", "state": "idle"}' > $TEST_KUGETSU_DIR/sessions/base.json + echo '{"type": "pm_agent", "opencode_session_id": "ses_test_pm_456", "created_at": "2026-03-29T18:00:00+02:00", "state": "idle"}' > $TEST_KUGETSU_DIR/sessions/pm-agent.json } # Test C1: Agent count file is initialized to 0 echo "--- Test: agent count file initialized ---" test_cleanup -mkdir -p ~/.kugetsu/sessions ~/.kugetsu/worktrees +mkdir -p $TEST_KUGETSU_DIR/sessions $TEST_KUGETSU_DIR/worktrees $KUGETSU list > /dev/null 2>&1 || true -if [ -f ~/.kugetsu/.agent_count ]; then - COUNT=$(cat ~/.kugetsu/.agent_count) +if [ -f $TEST_KUGETSU_DIR/.agent_count ]; then + COUNT=$(cat $TEST_KUGETSU_DIR/.agent_count) if [ "$COUNT" = "0" ]; then pass "agent count file initialized to 0" else @@ -795,10 +797,10 @@ test_cleanup setup_mock_sessions # Initialize count to 0 -echo 0 > ~/.kugetsu/.agent_count +echo 0 > $TEST_KUGETSU_DIR/.agent_count # Verify initial state -INITIAL=$(cat ~/.kugetsu/.agent_count) +INITIAL=$(cat $TEST_KUGETSU_DIR/.agent_count) if [ "$INITIAL" = "0" ]; then pass "agent count starts at 0" else @@ -809,7 +811,7 @@ fi $KUGETSU list > /dev/null 2>&1 # Verify count is still 0 (no slot leak) -AFTER=$(cat ~/.kugetsu/.agent_count) +AFTER=$(cat $TEST_KUGETSU_DIR/.agent_count) if [ "$AFTER" = "0" ]; then pass "agent count stays 0 after list (no leak)" else