From 654e03dc712184d55cd8c9cf02081eec718cb22b Mon Sep 17 00:00:00 2001 From: Alex Mikhalev Date: Wed, 18 Feb 2026 12:45:18 +0100 Subject: [PATCH] feat(gitea-skill): Add integration tests and fix workflow label swap - Add test-gitea-skill.sh with 10 tests (26 assertions) against live Gitea - Fix: API POST /labels only appends, does not enforce scoped exclusivity - Fix: Use PUT with label swap pattern to atomically replace workflow labels - Fix: Use PATCH /issues/{index} for assignees (POST /assignees not in 1.22.6) - All 26 assertions pass against live Gitea 1.22.6 Co-Authored-By: Claude Opus 4.6 --- .agents/skills/gitea/SKILL.md | 42 ++- .agents/skills/gitea/test-gitea-skill.sh | 429 +++++++++++++++++++++++ 2 files changed, 457 insertions(+), 14 deletions(-) create mode 100755 .agents/skills/gitea/test-gitea-skill.sh diff --git a/.agents/skills/gitea/SKILL.md b/.agents/skills/gitea/SKILL.md index f636d76..2b90974 100644 --- a/.agents/skills/gitea/SKILL.md +++ b/.agents/skills/gitea/SKILL.md @@ -228,17 +228,37 @@ Inspired by the br (beads_rust) pattern: ready -> claim -> implement -> close. +---------------+ ``` -Scoped labels enforce mutual exclusivity: applying `workflow:in-progress` auto-removes `workflow:ready`. +Scoped labels are visually exclusive in the Gitea UI, but the API POST endpoint only appends labels. The helpers below use a swap pattern: get current labels, remove old workflow labels, add the new one, then PUT the full set. + +### Swap Workflow Label (internal helper) + +```bash +gitea_swap_workflow() { + local index="$1" new_label="$2" owner="${3:-$GITEA_OWNER}" repo="${4:-$GITEA_REPO}" + local repo_labels issue_labels new_id kept_ids all_ids + + # Get the ID of the new workflow label + repo_labels=$(gitea_api GET "/repos/$owner/$repo/labels") + new_id=$(echo "$repo_labels" | jq -r --arg n "$new_label" '.[] | select(.name==$n) | .id') + + # Get current issue label IDs, filtering out all workflow:* labels + issue_labels=$(gitea_api GET "/repos/$owner/$repo/issues/$index") + kept_ids=$(echo "$issue_labels" | jq '[.labels[] | select(.name | startswith("workflow:") | not) | .id]') + + # Combine kept labels + new workflow label + all_ids=$(echo "$kept_ids" | jq --argjson nid "$new_id" '. + [$nid]') + + # Replace all labels atomically + gitea_api PUT "/repos/$owner/$repo/issues/$index/labels" "{\"labels\": $all_ids}" +} +``` ### Mark Ready ```bash gitea_ready() { local index="$1" owner="${2:-$GITEA_OWNER}" repo="${3:-$GITEA_REPO}" - local ready_id - ready_id=$(gitea_api GET "/repos/$owner/$repo/labels" | \ - jq -r '.[] | select(.name=="workflow:ready") | .id') - gitea_api POST "/repos/$owner/$repo/issues/$index/labels" "{\"labels\": [$ready_id]}" + gitea_swap_workflow "$index" "workflow:ready" "$owner" "$repo" } ``` @@ -247,12 +267,9 @@ gitea_ready() { ```bash gitea_claim() { local index="$1" user="${2:-}" owner="${3:-$GITEA_OWNER}" repo="${4:-$GITEA_REPO}" - local ip_id - ip_id=$(gitea_api GET "/repos/$owner/$repo/labels" | \ - jq -r '.[] | select(.name=="workflow:in-progress") | .id') - gitea_api POST "/repos/$owner/$repo/issues/$index/labels" "{\"labels\": [$ip_id]}" + gitea_swap_workflow "$index" "workflow:in-progress" "$owner" "$repo" if [ -n "$user" ]; then - gitea_api POST "/repos/$owner/$repo/issues/$index/assignees" "{\"assignees\": [\"$user\"]}" + gitea_api PATCH "/repos/$owner/$repo/issues/$index" "{\"assignees\": [\"$user\"]}" fi } ``` @@ -262,10 +279,7 @@ gitea_claim() { ```bash gitea_close_task() { local index="$1" comment="${2:-}" owner="${3:-$GITEA_OWNER}" repo="${4:-$GITEA_REPO}" - local done_id - done_id=$(gitea_api GET "/repos/$owner/$repo/labels" | \ - jq -r '.[] | select(.name=="workflow:done") | .id') - gitea_api POST "/repos/$owner/$repo/issues/$index/labels" "{\"labels\": [$done_id]}" + gitea_swap_workflow "$index" "workflow:done" "$owner" "$repo" gitea_api PATCH "/repos/$owner/$repo/issues/$index" '{"state": "closed"}' if [ -n "$comment" ]; then gitea_api POST "/repos/$owner/$repo/issues/$index/comments" \ diff --git a/.agents/skills/gitea/test-gitea-skill.sh b/.agents/skills/gitea/test-gitea-skill.sh new file mode 100755 index 0000000..ca2ac1b --- /dev/null +++ b/.agents/skills/gitea/test-gitea-skill.sh @@ -0,0 +1,429 @@ +#!/usr/bin/env bash +# test-gitea-skill.sh -- Integration tests for Gitea agent skill +# Runs 10 tests against a live Gitea instance. No mocks. +# +# Usage: +# source ~/op_zesticai_non_prod.sh +# export GITEA_TOKEN=$(op read "op://TerraphimPlatform/git.terraphim.cloud-admin-token/credential") +# ./test-gitea-skill.sh +# +# Environment: +# GITEA_URL -- Gitea instance (default: https://git.terraphim.cloud) +# GITEA_TOKEN -- API token (required) + +set -euo pipefail + +GITEA_URL="${GITEA_URL:-https://git.terraphim.cloud}" +TEST_OWNER="" +TEST_REPO="skill-test-$(date +%s)" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Counters +PASS=0 +FAIL=0 +TOTAL=0 + +# --- Colors --- +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +NC='\033[0m' # No Color + +# --- Helpers --- + +gitea_api() { + local method="$1" endpoint="$2" data="${3:-}" + local args=(-s -H "Authorization: token $GITEA_TOKEN" -H "Content-Type: application/json") + if [ -n "$data" ]; then + args+=(-X "$method" -d "$data") + else + args+=(-X "$method") + fi + curl "${args[@]}" "$GITEA_URL/api/v1$endpoint" +} + +gitea_api_code() { + local method="$1" endpoint="$2" data="${3:-}" + local args=(-s -o /dev/null -w '%{http_code}' -H "Authorization: token $GITEA_TOKEN" -H "Content-Type: application/json") + if [ -n "$data" ]; then + args+=(-X "$method" -d "$data") + else + args+=(-X "$method") + fi + curl "${args[@]}" "$GITEA_URL/api/v1$endpoint" +} + +log_pass() { + PASS=$((PASS + 1)) + TOTAL=$((TOTAL + 1)) + echo -e " ${GREEN}[PASS]${NC} $1" +} + +log_fail() { + FAIL=$((FAIL + 1)) + TOTAL=$((TOTAL + 1)) + echo -e " ${RED}[FAIL]${NC} $1" + if [ -n "${2:-}" ]; then + echo -e " Detail: $2" + fi +} + +assert_json_field() { + local json="$1" field="$2" expected="$3" msg="$4" + local actual + actual=$(echo "$json" | jq -r "$field" 2>/dev/null) + if [ "$actual" = "$expected" ]; then + log_pass "$msg" + else + log_fail "$msg" "expected '$expected', got '$actual'" + fi +} + +assert_json_nonzero() { + local json="$1" field="$2" msg="$3" + local actual + actual=$(echo "$json" | jq -r "$field" 2>/dev/null) + if [ -n "$actual" ] && [ "$actual" != "null" ] && [ "$actual" != "0" ]; then + log_pass "$msg" + else + log_fail "$msg" "field '$field' is empty, null, or 0" + fi +} + +assert_http_code() { + local actual="$1" expected="$2" msg="$3" + if [ "$actual" = "$expected" ]; then + log_pass "$msg" + else + log_fail "$msg" "expected HTTP $expected, got HTTP $actual" + fi +} + +# --- Pre-flight checks --- + +preflight() { + echo "=== Pre-flight Checks ===" + + if [ -z "${GITEA_TOKEN:-}" ]; then + echo -e "${RED}GITEA_TOKEN is not set. Run:${NC}" + echo " source ~/op_zesticai_non_prod.sh" + echo ' export GITEA_TOKEN=$(op read "op://TerraphimPlatform/git.terraphim.cloud-admin-token/credential")' + exit 1 + fi + + # Check authentication and get username + local user_info + user_info=$(gitea_api GET "/user") + TEST_OWNER=$(echo "$user_info" | jq -r '.login // empty') + + if [ -z "$TEST_OWNER" ]; then + echo -e "${RED}Authentication failed. Check GITEA_TOKEN.${NC}" + exit 1 + fi + + echo -e " Instance: $GITEA_URL" + echo -e " User: $TEST_OWNER" + echo -e " Test repo: $TEST_OWNER/$TEST_REPO" + echo "" +} + +# --- Cleanup --- + +cleanup() { + echo "" + echo "=== Cleanup ===" + local code + code=$(gitea_api_code DELETE "/repos/$TEST_OWNER/$TEST_REPO") + if [ "$code" = "204" ] || [ "$code" = "404" ]; then + echo -e " ${GREEN}Cleaned up${NC} $TEST_OWNER/$TEST_REPO" + else + echo -e " ${YELLOW}Warning:${NC} Cleanup returned HTTP $code for $TEST_OWNER/$TEST_REPO" + fi +} + +# Register cleanup on exit +trap cleanup EXIT + +# === Tests === + +test_01_create_repo() { + echo "--- Test 1: Create test repository ---" + local result + result=$(gitea_api POST "/user/repos" \ + "{\"name\": \"$TEST_REPO\", \"description\": \"Temporary test repo for Gitea skill tests\", \"auto_init\": true, \"private\": true}") + + assert_json_field "$result" ".name" "$TEST_REPO" "Repo name matches" + assert_json_nonzero "$result" ".id" "Repo has id" + assert_json_field "$result" ".owner.login" "$TEST_OWNER" "Owner matches" +} + +test_02_create_labels() { + echo "--- Test 2: Create labels via setup-labels.sh ---" + + # Run setup-labels.sh against test repo + local output + output=$(GITEA_OWNER="$TEST_OWNER" GITEA_REPO="$TEST_REPO" "$SCRIPT_DIR/setup-labels.sh" 2>&1) + + # Verify all 12 labels exist + local labels + labels=$(gitea_api GET "/repos/$TEST_OWNER/$TEST_REPO/labels") + local count + count=$(echo "$labels" | jq 'length') + + if [ "$count" -ge 12 ]; then + log_pass "Created $count labels (expected >= 12)" + else + log_fail "Label count" "expected >= 12, got $count" + fi + + # Verify specific scoped labels + local workflow_ready + workflow_ready=$(echo "$labels" | jq -r '.[] | select(.name=="workflow:ready") | .exclusive') + assert_json_field "{\"val\": $workflow_ready}" ".val" "true" "workflow:ready is exclusive" + + local workflow_ip + workflow_ip=$(echo "$labels" | jq -r '.[] | select(.name=="workflow:in-progress") | .exclusive') + assert_json_field "{\"val\": $workflow_ip}" ".val" "true" "workflow:in-progress is exclusive" +} + +test_03_create_milestone() { + echo "--- Test 3: Create milestone (project) ---" + local result + result=$(gitea_api POST "/repos/$TEST_OWNER/$TEST_REPO/milestones" \ + "$(jq -n --arg t "Test Sprint $(date +%Y-W%V)" --arg d "Integration test milestone" \ + '{title: $t, description: $d}')") + + MILESTONE_ID=$(echo "$result" | jq -r '.id // empty') + + if [ -n "$MILESTONE_ID" ] && [ "$MILESTONE_ID" != "null" ]; then + log_pass "Milestone created with id=$MILESTONE_ID" + else + log_fail "Milestone creation" "no id returned" + fi + + assert_json_nonzero "$result" ".title" "Milestone has title" +} + +test_04_create_issue() { + echo "--- Test 4: Create issue (task) with milestone ---" + + # Get label IDs for workflow:ready and type:task + local labels + labels=$(gitea_api GET "/repos/$TEST_OWNER/$TEST_REPO/labels") + local ready_id type_task_id + ready_id=$(echo "$labels" | jq -r '.[] | select(.name=="workflow:ready") | .id') + type_task_id=$(echo "$labels" | jq -r '.[] | select(.name=="type:task") | .id') + + local result + result=$(gitea_api POST "/repos/$TEST_OWNER/$TEST_REPO/issues" \ + "$(jq -n \ + --arg t "Test task: implement feature X" \ + --arg b "This is a test issue for the Gitea skill integration test." \ + --argjson m "$MILESTONE_ID" \ + --argjson labels "[$ready_id, $type_task_id]" \ + '{title: $t, body: $b, milestone: $m, labels: $labels}')") + + ISSUE_NUMBER=$(echo "$result" | jq -r '.number // empty') + + if [ -n "$ISSUE_NUMBER" ] && [ "$ISSUE_NUMBER" != "null" ]; then + log_pass "Issue created with number=#$ISSUE_NUMBER" + else + log_fail "Issue creation" "no number returned" + fi + + assert_json_field "$result" ".state" "open" "Issue state is open" + + # Verify milestone assignment + local milestone_id + milestone_id=$(echo "$result" | jq -r '.milestone.id // empty') + assert_json_field "{\"val\": \"$milestone_id\"}" ".val" "$MILESTONE_ID" "Milestone assigned" +} + +test_05_mark_ready() { + echo "--- Test 5: Mark task ready (workflow:ready) ---" + + # Use the swap pattern: get current labels, remove workflow:*, add workflow:ready, PUT + local repo_labels issue ready_id kept_ids all_ids + repo_labels=$(gitea_api GET "/repos/$TEST_OWNER/$TEST_REPO/labels") + ready_id=$(echo "$repo_labels" | jq -r '.[] | select(.name=="workflow:ready") | .id') + + issue=$(gitea_api GET "/repos/$TEST_OWNER/$TEST_REPO/issues/$ISSUE_NUMBER") + kept_ids=$(echo "$issue" | jq '[.labels[] | select(.name | startswith("workflow:") | not) | .id]') + all_ids=$(echo "$kept_ids" | jq --argjson nid "$ready_id" '. + [$nid]') + + gitea_api PUT "/repos/$TEST_OWNER/$TEST_REPO/issues/$ISSUE_NUMBER/labels" \ + "{\"labels\": $all_ids}" > /dev/null + + # Verify label is set + issue=$(gitea_api GET "/repos/$TEST_OWNER/$TEST_REPO/issues/$ISSUE_NUMBER") + local has_ready + has_ready=$(echo "$issue" | jq '[.labels[].name] | any(. == "workflow:ready")') + assert_json_field "{\"val\": $has_ready}" ".val" "true" "Issue has workflow:ready label" +} + +test_06_claim_task() { + echo "--- Test 6: Claim task (workflow:in-progress) ---" + + # Use the swap pattern: get current labels, remove workflow:*, add workflow:in-progress, PUT + local repo_labels issue ip_id kept_ids all_ids + repo_labels=$(gitea_api GET "/repos/$TEST_OWNER/$TEST_REPO/labels") + ip_id=$(echo "$repo_labels" | jq -r '.[] | select(.name=="workflow:in-progress") | .id') + + issue=$(gitea_api GET "/repos/$TEST_OWNER/$TEST_REPO/issues/$ISSUE_NUMBER") + kept_ids=$(echo "$issue" | jq '[.labels[] | select(.name | startswith("workflow:") | not) | .id]') + all_ids=$(echo "$kept_ids" | jq --argjson nid "$ip_id" '. + [$nid]') + + gitea_api PUT "/repos/$TEST_OWNER/$TEST_REPO/issues/$ISSUE_NUMBER/labels" \ + "{\"labels\": $all_ids}" > /dev/null + + # Assign to current user via PATCH (POST /assignees does not exist in Gitea 1.22.6) + gitea_api PATCH "/repos/$TEST_OWNER/$TEST_REPO/issues/$ISSUE_NUMBER" \ + "{\"assignees\": [\"$TEST_OWNER\"]}" > /dev/null + + # Verify + issue=$(gitea_api GET "/repos/$TEST_OWNER/$TEST_REPO/issues/$ISSUE_NUMBER") + + local has_ip + has_ip=$(echo "$issue" | jq '[.labels[].name] | any(. == "workflow:in-progress")') + assert_json_field "{\"val\": $has_ip}" ".val" "true" "Issue has workflow:in-progress label" + + # Check that workflow:ready was removed by label swap + local has_ready + has_ready=$(echo "$issue" | jq '[.labels[].name] | any(. == "workflow:ready")') + assert_json_field "{\"val\": $has_ready}" ".val" "false" "workflow:ready removed by label swap" + + # Check assignee + local assignee + assignee=$(echo "$issue" | jq -r '.assignees[0].login // empty') + assert_json_field "{\"val\": \"$assignee\"}" ".val" "$TEST_OWNER" "Assignee set" +} + +test_07_check_progress() { + echo "--- Test 7: Check project progress (0% complete) ---" + + local milestone + milestone=$(gitea_api GET "/repos/$TEST_OWNER/$TEST_REPO/milestones/$MILESTONE_ID") + + local open closed + open=$(echo "$milestone" | jq '.open_issues') + closed=$(echo "$milestone" | jq '.closed_issues') + + assert_json_field "{\"val\": $open}" ".val" "1" "1 open issue" + assert_json_field "{\"val\": $closed}" ".val" "0" "0 closed issues" + + # Calculate progress + local total pct + total=$((open + closed)) + if [ "$total" -gt 0 ]; then + pct=$((closed * 100 / total)) + else + pct=0 + fi + assert_json_field "{\"val\": $pct}" ".val" "0" "Progress is 0%" +} + +test_08_close_task() { + echo "--- Test 8: Close task ---" + + # Use the swap pattern: get current labels, remove workflow:*, add workflow:done, PUT + local repo_labels issue done_id kept_ids all_ids + repo_labels=$(gitea_api GET "/repos/$TEST_OWNER/$TEST_REPO/labels") + done_id=$(echo "$repo_labels" | jq -r '.[] | select(.name=="workflow:done") | .id') + + issue=$(gitea_api GET "/repos/$TEST_OWNER/$TEST_REPO/issues/$ISSUE_NUMBER") + kept_ids=$(echo "$issue" | jq '[.labels[] | select(.name | startswith("workflow:") | not) | .id]') + all_ids=$(echo "$kept_ids" | jq --argjson nid "$done_id" '. + [$nid]') + + gitea_api PUT "/repos/$TEST_OWNER/$TEST_REPO/issues/$ISSUE_NUMBER/labels" \ + "{\"labels\": $all_ids}" > /dev/null + + # Close the issue + gitea_api PATCH "/repos/$TEST_OWNER/$TEST_REPO/issues/$ISSUE_NUMBER" \ + '{"state": "closed"}' > /dev/null + + # Add closing comment + gitea_api POST "/repos/$TEST_OWNER/$TEST_REPO/issues/$ISSUE_NUMBER/comments" \ + "$(jq -n --arg b "Closed by integration test" '{body: $b}')" > /dev/null + + # Verify + issue=$(gitea_api GET "/repos/$TEST_OWNER/$TEST_REPO/issues/$ISSUE_NUMBER") + + assert_json_field "$issue" ".state" "closed" "Issue state is closed" + + local has_done + has_done=$(echo "$issue" | jq '[.labels[].name] | any(. == "workflow:done")') + assert_json_field "{\"val\": $has_done}" ".val" "true" "Issue has workflow:done label" + + # Check that workflow:in-progress was removed by label swap + local has_ip + has_ip=$(echo "$issue" | jq '[.labels[].name] | any(. == "workflow:in-progress")') + assert_json_field "{\"val\": $has_ip}" ".val" "false" "workflow:in-progress removed by label swap" +} + +test_09_progress_after_close() { + echo "--- Test 9: Check progress after close (100% complete) ---" + + local milestone + milestone=$(gitea_api GET "/repos/$TEST_OWNER/$TEST_REPO/milestones/$MILESTONE_ID") + + local open closed + open=$(echo "$milestone" | jq '.open_issues') + closed=$(echo "$milestone" | jq '.closed_issues') + + assert_json_field "{\"val\": $open}" ".val" "0" "0 open issues" + assert_json_field "{\"val\": $closed}" ".val" "1" "1 closed issue" + + # Calculate progress + local total pct + total=$((open + closed)) + if [ "$total" -gt 0 ]; then + pct=$((closed * 100 / total)) + else + pct=0 + fi + assert_json_field "{\"val\": $pct}" ".val" "100" "Progress is 100%" +} + +test_10_cleanup() { + echo "--- Test 10: Delete test repository ---" + local code + code=$(gitea_api_code DELETE "/repos/$TEST_OWNER/$TEST_REPO") + assert_http_code "$code" "204" "Repository deleted (HTTP 204)" + + # Verify it's gone + local verify_code + verify_code=$(gitea_api_code GET "/repos/$TEST_OWNER/$TEST_REPO") + assert_http_code "$verify_code" "404" "Repository confirmed deleted (HTTP 404)" + + # Disable the trap since we already cleaned up + trap - EXIT +} + +# === Main === + +echo "============================================" +echo " Gitea Skill Integration Tests" +echo "============================================" +echo "" + +preflight + +test_01_create_repo +test_02_create_labels +test_03_create_milestone +test_04_create_issue +test_05_mark_ready +test_06_claim_task +test_07_check_progress +test_08_close_task +test_09_progress_after_close +test_10_cleanup + +echo "" +echo "============================================" +echo -e " Results: ${GREEN}$PASS passed${NC}, ${RED}$FAIL failed${NC} (${TOTAL} total)" +echo "============================================" + +if [ "$FAIL" -gt 0 ]; then + exit 1 +fi