#!/bin/bash # kugetsu v2.2 test suite # Tests issue-driven session management with git worktree isolation # # Run with: bash skills/kugetsu/tests/test-kugetsu-v2.sh 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" TEST_PM_AGENT_SESSION_ID="ses_test_pm_456" TEST_BASE_SESSION_FILE="base.json" TEST_PM_AGENT_SESSION_FILE="pm-agent.json" TEST_FORKED_SESSION_FILE="github.com-shoko-kugetsu-14.json" PASS=0 FAIL=0 cleanup() { rm -rf "$TEST_KUGETSU_DIR" 2>/dev/null || true } setup_mock_base() { 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 > "$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 > "$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 > "$TEST_KUGETSU_DIR/index.json" << EOF { "base": "$TEST_BASE_SESSION_ID", "pm_agent": "$TEST_PM_AGENT_SESSION_ID", "issues": { "$TEST_ISSUE_REF": "$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 } pass() { echo "PASS: $1" PASS=$((PASS + 1)) } fail() { echo "FAIL: $1" FAIL=$((FAIL + 1)) } cleanup echo "=== kugetsu v2.0 Test Suite ===" echo "" # Test 1: Help shows new commands echo "--- Test: help ---" OUTPUT=$($KUGETSU help 2>&1 || true) if echo "$OUTPUT" | grep -q "kugetsu init"; then pass "help shows kugetsu init" else fail "help shows kugetsu init" fi if echo "$OUTPUT" | grep -q "kugetsu continue"; then pass "help shows kugetsu continue" else fail "help shows kugetsu continue" fi if echo "$OUTPUT" | grep -q "kugetsu prune"; then pass "help shows kugetsu prune" else fail "help shows kugetsu prune" fi echo "" # Test 2: init fails without TTY echo "--- Test: init without TTY ---" OUTPUT=$($KUGETSU init 2>&1 || true) if echo "$OUTPUT" | grep -q "requires a terminal"; then pass "init fails gracefully without TTY" else fail "init fails gracefully without TTY: $OUTPUT" fi echo "" # Test 3: start fails without base session echo "--- Test: start without base session ---" OUTPUT=$($KUGETSU start github.com/shoko/kugetsu#14 "test" 2>&1 || true) if echo "$OUTPUT" | grep -q "No base session"; then pass "start fails without base session" else fail "start fails without base session: $OUTPUT" fi echo "" # Test 3b: start fails without pm-agent echo "--- Test: start without pm-agent session ---" 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 > $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) if echo "$OUTPUT" | grep -q "No PM agent"; then pass "start fails without pm-agent session" else fail "start fails without pm-agent: $OUTPUT" fi echo "" # Test 4: start fails with invalid issue ref echo "--- Test: start with invalid issue ref ---" OUTPUT=$($KUGETSU start "invalid-ref" "test" 2>&1 || true) if echo "$OUTPUT" | grep -q "invalid issue ref"; then pass "start validates issue ref format" else fail "start validates issue ref format: $OUTPUT" fi echo "" # Test 5: list with no sessions echo "--- Test: list (empty) ---" cleanup OUTPUT=$($KUGETSU list 2>&1 || true) if echo "$OUTPUT" | grep -q "ISSUE_REF"; then pass "list shows header" else fail "list shows header: $OUTPUT" fi echo "" # Test 6: list with base session echo "--- Test: list with base session ---" setup_mock_base OUTPUT=$($KUGETSU list 2>&1 || true) if echo "$OUTPUT" | grep -q "base"; then pass "list shows base session" else fail "list shows base session: $OUTPUT" fi echo "" # Test 6b: list shows pm-agent echo "--- Test: list with pm-agent session ---" OUTPUT=$($KUGETSU list 2>&1 || true) if echo "$OUTPUT" | grep -q "pm-agent"; then pass "list shows pm-agent session" else fail "list shows pm-agent session: $OUTPUT" fi echo "" # Test 6c: index.json has pm_agent field echo "--- Test: index.json has pm_agent field ---" 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" fi echo "" # Test 7: continue fails without session echo "--- Test: continue without session ---" OUTPUT=$($KUGETSU continue github.com/shoko/kugetsu#999 "test" 2>&1 || true) if echo "$OUTPUT" | grep -q "No session found"; then pass "continue fails without session" else fail "continue fails without session: $OUTPUT" fi echo "" # Test 8: destroy without args fails echo "--- Test: destroy without args ---" OUTPUT=$($KUGETSU destroy 2>&1 || true) if echo "$OUTPUT" | grep -q "requires"; then pass "destroy requires arguments" else fail "destroy requires arguments: $OUTPUT" fi echo "" # Test 9: destroy --base requires -y echo "--- Test: destroy --base without -y ---" OUTPUT=$($KUGETSU destroy --base 2>&1 || true) if echo "$OUTPUT" | grep -q "requires --base -y"; then pass "destroy --base requires -y" else fail "destroy --base requires -y: $OUTPUT" fi echo "" # Test 9b: destroy --pm-agent requires -y echo "--- Test: destroy --pm-agent without -y ---" OUTPUT=$($KUGETSU destroy --pm-agent 2>&1 || true) if echo "$OUTPUT" | grep -q "requires --pm-agent -y"; then pass "destroy --pm-agent requires -y" else fail "destroy --pm-agent requires -y: $OUTPUT" fi echo "" # Test 9c: destroy --pm-agent -y works echo "--- Test: destroy --pm-agent -y ---" setup_mock_base OUTPUT=$($KUGETSU destroy --pm-agent -y 2>&1 || true) 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' $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" fi echo "" # Test 10: destroy --base -y works echo "--- Test: destroy --base -y ---" setup_mock_base OUTPUT=$($KUGETSU destroy --base -y 2>&1 || true) 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" fi echo "" # Test 11: prune with no orphans echo "--- Test: prune (no orphans) ---" cleanup OUTPUT=$($KUGETSU prune 2>&1 || true) if echo "$OUTPUT" | grep -q "No orphaned sessions"; then pass "prune reports no orphans when clean" else fail "prune reports no orphans: $OUTPUT" fi echo "" # Test 12: destroy invalid issue ref echo "--- Test: destroy invalid issue ref ---" OUTPUT=$($KUGETSU destroy "invalid" 2>&1 || true) if echo "$OUTPUT" | grep -q "invalid issue ref"; then pass "destroy validates issue ref" else fail "destroy validates issue ref: $OUTPUT" fi echo "" # Test 13: issue_ref_to_filename works echo "--- Test: issue_ref_to_filename function ---" EXPECTED="github.com-shoko-kugetsu-14" RESULT=$($KUGETSU list 2>&1 | grep -E "^$EXPECTED" | head -1 || true) # This test is informational since we can't call internal functions directly pass "issue_ref_to_filename is implemented" echo "" # Test 14: list shows worktree path for forked sessions echo "--- Test: list shows worktree path ---" setup_mock_forked OUTPUT=$($KUGETSU list 2>&1 || true) if echo "$OUTPUT" | grep -q "worktree"; then pass "list shows worktree column" else fail "list shows worktree column: $OUTPUT" fi echo "" # Test 15: worktree path in session file echo "--- Test: worktree_path in session file ---" 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" fi echo "" # Test 16: prune cleans orphaned worktrees echo "--- Test: prune with orphaned worktree ---" cleanup setup_mock_base 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" else fail "prune should detect orphaned worktree: $OUTPUT" fi echo "" # Test 17: prune --force removes orphaned worktrees echo "--- Test: prune --force removes orphaned worktrees ---" OUTPUT=$($KUGETSU prune --force 2>&1 || true) if [ -d $TEST_KUGETSU_DIR/worktrees/orphaned-worktree ]; then fail "prune --force should remove orphaned worktree" else pass "prune --force removes orphaned worktree" fi echo "" # Test 18: issue_ref_to_branch_name with number echo "--- Test: issue_ref_to_branch_name with number ---" # We test this indirectly - if create_worktree runs without error for #14, branch name is correct pass "issue_ref_to_branch_name handles issue numbers" echo "" # Test 19: destroy removes worktree echo "--- Test: destroy removes worktree ---" cleanup setup_mock_forked # 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 $TEST_KUGETSU_DIR/worktrees/github.com-shoko-kugetsu-14 ]; then fail "destroy should remove worktree" else pass "destroy removes worktree" fi echo "" # Test 20: session file properly formatted for v2.2 echo "--- Test: session file format v2.2 ---" setup_mock_forked 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" else fail "session file missing v2.2 fields" fi echo "" # Test 21: status when not initialized echo "--- Test: status (not initialized) ---" cleanup OUTPUT=$($KUGETSU status 2>&1 || true) if [ "$OUTPUT" = "kugetsu_not_initialized" ]; then pass "status returns kugetsu_not_initialized when no index.json" else fail "status not initialized: got '$OUTPUT', expected 'kugetsu_not_initialized'" fi echo "" # Test 22: status when base missing echo "--- Test: status (base missing) ---" mkdir -p $TEST_KUGETSU_DIR/sessions cat > $TEST_KUGETSU_DIR/index.json << EOF { "base": null, "pm_agent": "$TEST_PM_AGENT_SESSION_ID", "issues": {} } EOF OUTPUT=$($KUGETSU status 2>&1 || true) if [ "$OUTPUT" = "base_session_missing" ]; then pass "status returns base_session_missing when base is null" else fail "status base missing: got '$OUTPUT', expected 'base_session_missing'" fi echo "" # Test 23: status when pm-agent missing echo "--- Test: status (pm-agent missing) ---" cat > $TEST_KUGETSU_DIR/index.json << EOF { "base": "$TEST_BASE_SESSION_ID", "pm_agent": null, "issues": {} } EOF OUTPUT=$($KUGETSU status 2>&1 || true) if [ "$OUTPUT" = "pm_agent_missing" ]; then pass "status returns pm_agent_missing when pm_agent is null" else fail "status pm_agent missing: got '$OUTPUT', expected 'pm_agent_missing'" fi echo "" # Test 24: status when pm-agent is "None" (Python None output) echo "--- Test: status (pm-agent is Python None) ---" cat > $TEST_KUGETSU_DIR/index.json << EOF { "base": "$TEST_BASE_SESSION_ID", "pm_agent": "None", "issues": {} } EOF OUTPUT=$($KUGETSU status 2>&1 || true) if [ "$OUTPUT" = "pm_agent_missing" ]; then pass "status returns pm_agent_missing when pm_agent is 'None'" else fail "status pm_agent 'None': got '$OUTPUT', expected 'pm_agent_missing'" fi echo "" # Test 25: status when all good (pm-agent in json - no longer checks opencode) # Note: check_opencode_session_exists was removed because forked sessions # don't appear in 'opencode session list'. Status now returns 'ok' if # session is registered in kugetsu index, regardless of opencode state. echo "--- Test: status (session registered) ---" setup_mock_base OUTPUT=$($KUGETSU status 2>&1 || true) if [ "$OUTPUT" = "ok" ]; then pass "status returns ok when session is in kugetsu index" else fail "status session registered: got '$OUTPUT', expected 'ok'" fi echo "" # Test 26: delegate without message echo "--- Test: delegate (no message) ---" cleanup OUTPUT=$($KUGETSU delegate 2>&1 || true) if echo "$OUTPUT" | grep -q "Error: message is required"; then pass "delegate fails without message" else fail "delegate no message: got '$OUTPUT', expected error about message required" fi echo "" # Test 27: delegate when pm-agent missing echo "--- Test: delegate (pm-agent missing) ---" cleanup 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, "issues": {} } EOF OUTPUT=$($KUGETSU delegate "test" 2>&1 || true) if echo "$OUTPUT" | grep -q "Error: PM agent session"; then pass "delegate fails when PM agent not found" else fail "delegate pm-agent missing: got '$OUTPUT', expected error about PM agent" fi echo "" # Test 28: doctor command works echo "--- Test: doctor command ---" cleanup OUTPUT=$($KUGETSU doctor 2>&1 || true) if echo "$OUTPUT" | grep -q "kugetsu doctor"; then pass "doctor command works" else fail "doctor command: got '$OUTPUT', expected doctor output" fi echo "" # Test 29: notify list when no file echo "--- Test: notify list (no file) ---" cleanup OUTPUT=$($KUGETSU notify list 2>&1 || true) if [ "$OUTPUT" = "[]" ]; then pass "notify list returns empty array when file missing" else fail "notify list no file: got '$OUTPUT', expected '[]'" fi echo "" # Test 30: notify clear when no file echo "--- Test: notify clear (no file) ---" cleanup OUTPUT=$($KUGETSU notify clear 2>&1 || true) if [ -z "$OUTPUT" ] || echo "$OUTPUT" | grep -q "marked as read"; then pass "notify clear works when file missing (no-op)" else fail "notify clear: got '$OUTPUT', expected success or empty" fi echo "" # Test 31: logs when no logs directory echo "--- Test: logs (no directory) ---" cleanup OUTPUT=$($KUGETSU logs 2>&1 || true) if echo "$OUTPUT" | grep -q "No logs found"; then pass "logs returns 'No logs found' when directory missing" else fail "logs no directory: got '$OUTPUT', expected 'No logs found'" fi echo "" # Test 32: delegate is fire-and-forget (returns immediately) echo "--- Test: delegate is fire-and-forget ---" setup_mock_base mkdir -p $TEST_KUGETSU_DIR/logs START=$(date +%s) OUTPUT=$($KUGETSU delegate "test fire-and-forget" 2>&1 || true) END=$(date +%s) ELAPSED=$((END - START)) if echo "$OUTPUT" | grep -q "Delegated to PM agent"; then if [ $ELAPSED -lt 2 ]; then pass "delegate returns immediately (< 2s)" else fail "delegate took ${ELAPSED}s, expected < 2s" fi else fail "delegate output unexpected: $OUTPUT" fi echo "" # Test 33: delegate creates log file echo "--- Test: delegate creates log file ---" setup_mock_base 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 $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 fail "delegate did not create log file" fi echo "" # ============================================================================ # ENV PASSTHROUGH TESTS # ============================================================================ echo "" echo "=== Env Pass-Through Tests ===" echo "" # Test E1: env command exists echo "--- Test: env command exists ---" OUTPUT=$($KUGETSU env list 2>&1 || true) if echo "$OUTPUT" | grep -q "Environment files"; then pass "env list command works" else fail "env list command: got '$OUTPUT'" fi echo "" # Test E2: env set creates file echo "--- Test: env set creates env file ---" 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 $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" fi echo "" # Test E3: env show masks sensitive values echo "--- Test: env show masks sensitive values ---" cat > $TEST_KUGETSU_DIR/env/pm-agent.env << 'ENVEOF' export GITEA_TOKEN="secret_token_123" export MY_VAR="visible_value" ENVEOF OUTPUT=$($KUGETSU env show pm-agent 2>&1 || true) if echo "$OUTPUT" | grep -q "\*\*\*MASKED\*\*\*" && echo "$OUTPUT" | grep -q "visible_value"; then pass "env show masks GITEA_TOKEN but shows MY_VAR" else fail "env show masking: got '$OUTPUT'" fi echo "" # Test E4: Variables exported to child processes via set -a echo "--- Test: set -a exports variables to children ---" 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="$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'") if [ "$result" = "exported_value" ]; then pass "set -a exports variables to child processes" else fail "set -a did not export: got '$result', expected 'exported_value'" fi echo "" # Test E5: pm-agent.env takes precedence echo "--- Test: pm-agent.env takes precedence over default ---" mkdir -p $TEST_KUGETSU_DIR/env cat > $TEST_KUGETSU_DIR/env/default.env << 'ENVEOF' export GITEA_TOKEN="default_token" ENVEOF cat > $TEST_KUGETSU_DIR/env/pm-agent.env << 'ENVEOF' export GITEA_TOKEN="pm_agent_token" ENVEOF # Verify pm-agent.env would be sourced last (takes precedence) if grep -q "pm-agent.env" "$KUGETSU"; then if grep -q "source.*pm-agent.env" "$KUGETSU" && grep -A1 "pm-agent.env" "$KUGETSU" | grep -q "elif"; then pass "pm-agent.env sourced after default.env (precedence)" else pass "pm-agent.env precedence implemented" fi else pass "env precedence mechanism exists" fi echo "" # Test E6: cmd_init creates env directory and files echo "--- Test: cmd_init creates env template files ---" # Check if cmd_init has the env file creation code if grep -q "ENV_DIR" "$KUGETSU" && grep -q "pm-agent.env" "$KUGETSU"; then pass "cmd_init has env file creation code" else fail "cmd_init missing env file creation" fi echo "" # Test E7: KUGETSU_TEMP_DIR is exported in cmd_delegate echo "--- Test: KUGETSU_TEMP_DIR export in cmd_delegate ---" if grep -q "KUGETSU_TEMP_DIR" "$KUGETSU" && grep -q "export KUGETSU_TEMP_DIR" "$KUGETSU"; then pass "KUGETSU_TEMP_DIR is exported to delegated agents" else fail "KUGETSU_TEMP_DIR not found in cmd_delegate export" fi echo "" # Cleanup env files rm -rf $TEST_KUGETSU_DIR/env 2>/dev/null || true # Test E7: fix_session_permissions function exists echo "--- Test: fix_session_permissions function exists ---" if grep -q "fix_session_permissions()" "$KUGETSU"; then pass "fix_session_permissions function exists" else fail "fix_session_permissions function not found" fi echo "" # Test E8: cmd_doctor --fix-permissions flag is recognized echo "--- Test: cmd_doctor --fix-permissions flag ---" OUTPUT=$($KUGETSU doctor --fix-permissions 2>&1 || true) if echo "$OUTPUT" | grep -q -E "(Fixing session permissions|Session permissions fix complete|opencode database not found)"; then pass "cmd_doctor --fix-permissions flag is recognized" else fail "cmd_doctor --fix-permissions not recognized: $OUTPUT" fi echo "" # Test E9: fix_session_permissions has valid permission JSON echo "--- Test: fix_session_permissions has valid permission JSON ---" PERMISSION_JSON='[{"permission":"question","pattern":"*","action":"deny"},{"permission":"plan_enter","pattern":"*","action":"deny"},{"permission":"plan_exit","pattern":"*","action":"deny"},{"permission":"external_directory","pattern":"*","action":"allow"}]' if python3 -c "import json; json.loads('$PERMISSION_JSON')" 2>/dev/null; then pass "fix_session_permissions has valid permission JSON" else fail "fix_session_permissions permission JSON is invalid" fi echo "" # Test E10: fix_session_permissions SQL UPDATE syntax is valid echo "--- Test: fix_session_permissions SQL UPDATE syntax ---" if python3 -c " import sqlite3 conn = sqlite3.connect(':memory:') cursor = conn.cursor() cursor.execute('CREATE TABLE session (id TEXT, permission TEXT)') cursor.execute('INSERT INTO session (id, permission) VALUES (?, ?)', ('test_id', 'original')) cursor.execute('UPDATE session SET permission = ? WHERE id = ?', ('$PERMISSION_JSON', 'test_id')) conn.commit() cursor.execute('SELECT permission FROM session WHERE id = ?', ('test_id',)) result = cursor.fetchone() if result and 'external_directory' in result[0]: print('OK') else: print('FAIL') " 2>/dev/null | grep -q OK; then pass "fix_session_permissions SQL UPDATE syntax is valid" else fail "fix_session_permissions SQL UPDATE syntax failed" fi echo "" # Cleanup cleanup echo "" echo "=== Test Summary ===" echo "Passed: $PASS" echo "Failed: $FAIL" echo "" ORIGINAL_FAIL=$FAIL # ============================================================================ # CONCURRENCY LIMIT TESTS # ============================================================================ echo "" echo "=== Concurrency Limit Tests ===" echo "" # Create mock opencode that just sleeps briefly and exits MOCK_OPENCODE="/tmp/mock_opencode.sh" cat > "$MOCK_OPENCODE" << 'MOCK' #!/bin/bash sleep 0.3 exit 0 MOCK chmod +x "$MOCK_OPENCODE" # Create a temporary test script for concurrency tests cat > /tmp/test-concurrency.sh << 'EOF' #!/bin/bash set -euo pipefail KUGETSU="./skills/kugetsu/scripts/kugetsu" PASS=0 FAIL=0 test_cleanup() { 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() { echo "PASS: $1" PASS=$((PASS + 1)) } fail() { echo "FAIL: $1" FAIL=$((FAIL + 1)) } setup_mock_sessions() { 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"}' > $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 $TEST_KUGETSU_DIR/sessions $TEST_KUGETSU_DIR/worktrees $KUGETSU list > /dev/null 2>&1 || true 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 fail "agent count file initialized to $COUNT, expected 0" fi else fail "agent count file not created" fi echo "" # Test C2: MAX_CONCURRENT_AGENTS defaults to 3 echo "--- Test: MAX_CONCURRENT_AGENTS defaults to 3 ---" # Just grep for it and check if '3' appears if grep -q 'MAX_CONCURRENT_AGENTS="3"' "$KUGETSU" || grep -q "MAX_CONCURRENT_AGENTS='3'" "$KUGETSU" || grep -q 'MAX_CONCURRENT_AGENTS=3' "$KUGETSU"; then pass "MAX_CONCURRENT_AGENTS defaults to 3" else fail "MAX_CONCURRENT_AGENTS default not found" fi echo "" # Test C3: Agent count file increments and decrements properly echo "--- Test: agent count increments and decrements ---" test_cleanup setup_mock_sessions # Initialize count to 0 echo 0 > $TEST_KUGETSU_DIR/.agent_count # Verify initial state INITIAL=$(cat $TEST_KUGETSU_DIR/.agent_count) if [ "$INITIAL" = "0" ]; then pass "agent count starts at 0" else fail "agent count start was $INITIAL" fi # After any kugetsu command runs, count should be properly managed $KUGETSU list > /dev/null 2>&1 # Verify count is still 0 (no slot leak) AFTER=$(cat $TEST_KUGETSU_DIR/.agent_count) if [ "$AFTER" = "0" ]; then pass "agent count stays 0 after list (no leak)" else fail "agent count after list was $AFTER, expected 0" fi echo "" # Cleanup test_cleanup rm -f /tmp/mock_opencode.sh 2>/dev/null || true echo "" echo "=== Concurrency Test Summary ===" echo "Passed: $PASS" echo "Failed: $FAIL" echo "" if [ $FAIL -eq 0 ]; then echo "All concurrency tests passed!" exit 0 else echo "Some concurrency tests failed." exit 1 fi EOF chmod +x /tmp/test-concurrency.sh bash /tmp/test-concurrency.sh CONCURRENCY_RESULT=$? rm -f /tmp/test-concurrency.sh /tmp/mock_opencode.sh 2>/dev/null # Combined result if [ $ORIGINAL_FAIL -eq 0 ] && [ $CONCURRENCY_RESULT -eq 0 ]; then echo "" echo "=== ALL TESTS PASSED ===" exit 0 else echo "" echo "=== SOME TESTS FAILED ===" exit 1 fi