rdev/cookbooks/scripts/feature-test.sh
jordan 425ef0f806 feat: add SDLC orchestration - library, CLI, and API integration
Implements deterministic feature lifecycle management for agent-driven
development. Agents use the CLI in pods; operators control via REST API.

Library (internal/sdlc/):
- Feature lifecycle with 10 phases (draft → released)
- Classifier engine with priority-ordered rules
- Artifact tracking with approval workflow
- Task management within features
- YAML-based state persistence

CLI (cmd/sdlc/):
- init, state, next, feature, artifact, task, query commands
- --json flag for machine-readable output
- Runs inside project pods

API (21 endpoints under /projects/{id}/sdlc/):
- State: GET /state, GET /next
- Features: CRUD + transition/block/unblock
- Artifacts: approve/reject per type
- Tasks: add/start/complete/block
- Queries: blocked/ready/needs-approval

Architecture:
- Port: SDLCExecutor interface (internal/port/)
- Adapter: kubectl exec into pods (internal/adapter/kubernetes/)
- Service: pod resolution + logging (internal/service/)
- Handlers: 5 files under 500-line limit (internal/handlers/)

Also includes template upgrades (chassis framework, UI components,
OpenAPI helpers, backend/frontend guides) and component improvements.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 09:57:05 -07:00

466 lines
16 KiB
Bash
Executable File

#!/bin/bash
set -euo pipefail
# Feature Development E2E Test Script
# Tests the complete feature development workflow:
# 1. Create composable project with skeleton
# 2. Add service component
# 3. Add app-nextjs component
# 4. Verify chassis patterns are available
# 5. Verify design system packages exist
# 6. Test auth integration
# 7. Verify CI pipeline
#
# Usage: ./cookbooks/scripts/feature-test.sh <command> <project-name>
# Commands: run, status, verify-patterns, teardown
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/common.sh"
COMMAND="${1:-}"
PROJECT_NAME="${2:-}"
if [[ -z "$COMMAND" || -z "$PROJECT_NAME" ]]; then
echo "Usage: $0 <command> <project-name>"
echo "Commands:"
echo " run - Create project with full stack and verify patterns"
echo " status - Check project status"
echo " verify-patterns - Verify chassis and design system patterns exist"
echo " teardown - Delete the project"
exit 1
fi
# Verify chassis patterns in the created project
verify_chassis_patterns() {
local project_id="$1"
local git_owner
git_owner=$(get_git_owner)
print_header "Verifying Chassis Patterns"
# Check pkg/httperror exists
echo "Checking pkg/httperror..."
local httperror_check
httperror_check=$(curl -s "https://git.threesix.ai/api/v1/repos/$git_owner/$project_id/contents/pkg/httperror/error.go" \
-H "Authorization: token ${GITEA_TOKEN:-}" 2>/dev/null | jq -r '.name // "not found"')
if [[ "$httperror_check" == "error.go" ]]; then
print_success "pkg/httperror/error.go exists"
else
print_warning "pkg/httperror/error.go not found (may be named differently)"
fi
# Check pkg/app/handler.go (Wrap pattern)
echo "Checking pkg/app/handler.go (Wrap pattern)..."
local handler_check
handler_check=$(curl -s "https://git.threesix.ai/api/v1/repos/$git_owner/$project_id/contents/pkg/app/handler.go" \
-H "Authorization: token ${GITEA_TOKEN:-}" 2>/dev/null | jq -r '.name // "not found"')
if [[ "$handler_check" == "handler.go" ]]; then
print_success "pkg/app/handler.go exists (Wrap pattern)"
else
print_warning "pkg/app/handler.go not found"
fi
# Check pkg/app/bind.go
echo "Checking pkg/app/bind.go..."
local bind_check
bind_check=$(curl -s "https://git.threesix.ai/api/v1/repos/$git_owner/$project_id/contents/pkg/app/bind.go" \
-H "Authorization: token ${GITEA_TOKEN:-}" 2>/dev/null | jq -r '.name // "not found"')
if [[ "$bind_check" == "bind.go" ]]; then
print_success "pkg/app/bind.go exists (Bind pattern)"
else
print_warning "pkg/app/bind.go not found"
fi
# Check pkg/app/health.go
echo "Checking pkg/app/health.go..."
local health_check
health_check=$(curl -s "https://git.threesix.ai/api/v1/repos/$git_owner/$project_id/contents/pkg/app/health.go" \
-H "Authorization: token ${GITEA_TOKEN:-}" 2>/dev/null | jq -r '.name // "not found"')
if [[ "$health_check" == "health.go" ]]; then
print_success "pkg/app/health.go exists (Health probes)"
else
print_warning "pkg/app/health.go not found"
fi
# Check pkg/auth exists
echo "Checking pkg/auth..."
local auth_check
auth_check=$(curl -s "https://git.threesix.ai/api/v1/repos/$git_owner/$project_id/contents/pkg/auth" \
-H "Authorization: token ${GITEA_TOKEN:-}" 2>/dev/null | jq -r 'if type == "array" then "directory" else "not found" end')
if [[ "$auth_check" == "directory" ]]; then
print_success "pkg/auth/ directory exists"
else
print_warning "pkg/auth/ not found"
fi
# Check pkg/chassis exists
echo "Checking pkg/chassis..."
local chassis_check
chassis_check=$(curl -s "https://git.threesix.ai/api/v1/repos/$git_owner/$project_id/contents/pkg/chassis/chassis.go" \
-H "Authorization: token ${GITEA_TOKEN:-}" 2>/dev/null | jq -r '.name // "not found"')
if [[ "$chassis_check" == "chassis.go" ]]; then
print_success "pkg/chassis/chassis.go exists (facade)"
else
print_warning "pkg/chassis/chassis.go not found"
fi
# Check pkg/openapi exists
echo "Checking pkg/openapi..."
local openapi_check
openapi_check=$(curl -s "https://git.threesix.ai/api/v1/repos/$git_owner/$project_id/contents/pkg/openapi" \
-H "Authorization: token ${GITEA_TOKEN:-}" 2>/dev/null | jq -r 'if type == "array" then "directory" else "not found" end')
if [[ "$openapi_check" == "directory" ]]; then
print_success "pkg/openapi/ directory exists (spec builder + docs)"
else
print_warning "pkg/openapi/ not found"
fi
}
# Verify design system packages
verify_design_system() {
local project_id="$1"
local git_owner
git_owner=$(get_git_owner)
print_header "Verifying Design System Packages"
# Check packages/ui exists
echo "Checking packages/ui..."
local ui_check
ui_check=$(curl -s "https://git.threesix.ai/api/v1/repos/$git_owner/$project_id/contents/packages/ui/package.json" \
-H "Authorization: token ${GITEA_TOKEN:-}" 2>/dev/null | jq -r '.name // "not found"')
if [[ "$ui_check" == "package.json" ]]; then
print_success "packages/ui exists"
else
print_warning "packages/ui not found"
fi
# Check packages/layout exists
echo "Checking packages/layout..."
local layout_check
layout_check=$(curl -s "https://git.threesix.ai/api/v1/repos/$git_owner/$project_id/contents/packages/layout/package.json" \
-H "Authorization: token ${GITEA_TOKEN:-}" 2>/dev/null | jq -r '.name // "not found"')
if [[ "$layout_check" == "package.json" ]]; then
print_success "packages/layout exists"
else
print_warning "packages/layout not found"
fi
# Check packages/auth exists
echo "Checking packages/auth..."
local auth_check
auth_check=$(curl -s "https://git.threesix.ai/api/v1/repos/$git_owner/$project_id/contents/packages/auth/package.json" \
-H "Authorization: token ${GITEA_TOKEN:-}" 2>/dev/null | jq -r '.name // "not found"')
if [[ "$auth_check" == "package.json" ]]; then
print_success "packages/auth exists"
else
print_warning "packages/auth not found"
fi
# Check packages/api-client exists
echo "Checking packages/api-client..."
local client_check
client_check=$(curl -s "https://git.threesix.ai/api/v1/repos/$git_owner/$project_id/contents/packages/api-client/package.json" \
-H "Authorization: token ${GITEA_TOKEN:-}" 2>/dev/null | jq -r '.name // "not found"')
if [[ "$client_check" == "package.json" ]]; then
print_success "packages/api-client exists"
else
print_warning "packages/api-client not found"
fi
}
# Verify service component uses chassis patterns
verify_service_patterns() {
local project_id="$1"
local git_owner
git_owner=$(get_git_owner)
print_header "Verifying Service Component"
# Check services/api exists
echo "Checking services/api..."
local svc_check
svc_check=$(curl -s "https://git.threesix.ai/api/v1/repos/$git_owner/$project_id/contents/services/api/cmd/server/main.go" \
-H "Authorization: token ${GITEA_TOKEN:-}" 2>/dev/null | jq -r '.name // "not found"')
if [[ "$svc_check" == "main.go" ]]; then
print_success "services/api component exists"
else
print_warning "services/api not found"
fi
# Check services/api/internal/api/routes.go exists
echo "Checking routes.go..."
local routes_check
routes_check=$(curl -s "https://git.threesix.ai/api/v1/repos/$git_owner/$project_id/contents/services/api/internal/api/routes.go" \
-H "Authorization: token ${GITEA_TOKEN:-}" 2>/dev/null | jq -r '.name // "not found"')
if [[ "$routes_check" == "routes.go" ]]; then
print_success "services/api/internal/api/routes.go exists"
else
print_warning "routes.go not found"
fi
# Check services/api/internal/api/spec.go exists (OpenAPI)
echo "Checking spec.go (OpenAPI)..."
local spec_check
spec_check=$(curl -s "https://git.threesix.ai/api/v1/repos/$git_owner/$project_id/contents/services/api/internal/api/spec.go" \
-H "Authorization: token ${GITEA_TOKEN:-}" 2>/dev/null | jq -r '.name // "not found"')
if [[ "$spec_check" == "spec.go" ]]; then
print_success "services/api/internal/api/spec.go exists (OpenAPI spec)"
else
print_warning "spec.go not found"
fi
# Check services/api/internal/api/handlers/example_test.go exists
echo "Checking example_test.go..."
local test_check
test_check=$(curl -s "https://git.threesix.ai/api/v1/repos/$git_owner/$project_id/contents/services/api/internal/api/handlers/example_test.go" \
-H "Authorization: token ${GITEA_TOKEN:-}" 2>/dev/null | jq -r '.name // "not found"')
if [[ "$test_check" == "example_test.go" ]]; then
print_success "services/api/internal/api/handlers/example_test.go exists"
else
print_warning "example_test.go not found"
fi
}
# Verify app-nextjs component
verify_nextjs_app() {
local project_id="$1"
local git_owner
git_owner=$(get_git_owner)
print_header "Verifying Next.js App Component"
# Check apps/dashboard exists
echo "Checking apps/dashboard..."
local app_check
app_check=$(curl -s "https://git.threesix.ai/api/v1/repos/$git_owner/$project_id/contents/apps/dashboard/package.json" \
-H "Authorization: token ${GITEA_TOKEN:-}" 2>/dev/null | jq -r '.name // "not found"')
if [[ "$app_check" == "package.json" ]]; then
print_success "apps/dashboard exists"
else
print_warning "apps/dashboard not found"
fi
# Check App Router structure
echo "Checking App Router structure..."
local layout_check
layout_check=$(curl -s "https://git.threesix.ai/api/v1/repos/$git_owner/$project_id/contents/apps/dashboard/src/app/layout.tsx" \
-H "Authorization: token ${GITEA_TOKEN:-}" 2>/dev/null | jq -r '.name // "not found"')
if [[ "$layout_check" == "layout.tsx" ]]; then
print_success "App Router layout.tsx exists"
else
print_warning "App Router layout.tsx not found"
fi
# Check dashboard route group
echo "Checking (dashboard) route group..."
local dashboard_check
dashboard_check=$(curl -s "https://git.threesix.ai/api/v1/repos/$git_owner/$project_id/contents/apps/dashboard/src/app/(dashboard)/layout.tsx" \
-H "Authorization: token ${GITEA_TOKEN:-}" 2>/dev/null | jq -r '.name // "not found"')
if [[ "$dashboard_check" == "layout.tsx" ]]; then
print_success "(dashboard) route group exists"
else
print_warning "(dashboard) route group not found"
fi
}
# Add a component and verify
add_component() {
local comp_type="$1"
local comp_name="$2"
echo "Adding $comp_type component: $comp_name"
local payload
payload=$(jq -n \
--arg type "$comp_type" \
--arg name "$comp_name" \
'{type: $type, name: $name}')
local result
result=$(api_call POST "/projects/$PROJECT_NAME/components" "$payload")
local path
path=$(echo "$result" | jq -r '.data.path // .path // ""')
if [[ -z "$path" ]]; then
print_error "Failed to add component"
echo "$result" | jq '.'
return 1
fi
local port
port=$(echo "$result" | jq -r '.data.port // .port // "N/A"')
print_success "Added $comp_type/$comp_name at $path (port: $port)"
return 0
}
run_flow() {
print_header "Feature Development E2E Test"
echo "Project: $PROJECT_NAME"
# Step 1: Create project (skeleton)
print_header "Step 1: Creating project skeleton"
local create_payload
create_payload=$(jq -n \
--arg name "$PROJECT_NAME" \
--arg desc "Feature development E2E test" \
'{name: $name, description: $desc}')
local create_result
create_result=$(api_call POST "/project" "$create_payload")
echo "$create_result" | jq '.'
local domain
domain=$(echo "$create_result" | jq -r '.data.domain // .domain // ""')
if [[ -z "$domain" ]]; then
print_error "Failed to create project"
exit 1
fi
print_success "Project created with domain: $domain"
# Step 2: Add backend service
print_header "Step 2: Adding backend service"
if ! add_component "service" "api"; then
exit 1
fi
# Step 3: Add Next.js dashboard
print_header "Step 3: Adding Next.js dashboard"
if ! add_component "app-nextjs" "dashboard"; then
exit 1
fi
# Step 4: List components
print_header "Step 4: Verifying components"
local components
components=$(api_call GET "/projects/$PROJECT_NAME/components")
echo "$components" | jq '.data // .'
local comp_count
comp_count=$(echo "$components" | jq '.data.components | length // 0')
if [[ "$comp_count" -lt 2 ]]; then
print_warning "Expected 2 components, got $comp_count"
else
print_success "All components added successfully"
fi
# Step 5: Wait for initial commit to propagate
print_header "Step 5: Waiting for git to sync..."
sleep 10
# Step 6: Verify patterns
verify_chassis_patterns "$PROJECT_NAME"
verify_design_system "$PROJECT_NAME"
verify_service_patterns "$PROJECT_NAME"
verify_nextjs_app "$PROJECT_NAME"
# Step 7: Wait for CI pipeline
print_header "Step 7: Waiting for CI pipeline"
if ! wait_for_pipeline "$PROJECT_NAME"; then
print_warning "Pipeline may have issues, continuing to check site..."
fi
# Step 8: Wait for site
print_header "Step 8: Verifying site is accessible"
if ! wait_for_site "$domain" 30 5 "$PROJECT_NAME"; then
print_warning "Site not yet accessible (may still be deploying)"
fi
# Summary
print_header "E2E Test Results"
print_success "Project created: $PROJECT_NAME"
print_success "Components added: $comp_count"
echo ""
echo "Site URL: https://$domain"
echo "Git repo: https://git.threesix.ai/$(get_git_owner)/$PROJECT_NAME"
echo "CI: https://ci.threesix.ai/$(get_git_owner)/$PROJECT_NAME"
echo ""
echo "Next steps:"
echo " 1. Clone the repo and start developing features"
echo " 2. Use pkg/httperror, pkg/app for chassis patterns"
echo " 3. Use packages/ui, packages/layout for design system"
echo " 4. Use packages/auth for authentication"
echo " 5. Run ./scripts/generate-client.sh after OpenAPI changes"
}
check_status() {
print_header "Project Status: $PROJECT_NAME"
# Get project info
echo "Project:"
api_call GET "/project/$PROJECT_NAME" | jq '.data // .'
echo ""
# Get components
echo "Components:"
api_call GET "/projects/$PROJECT_NAME/components" | jq '.data // .'
echo ""
# Get latest pipelines
echo "Latest Pipelines:"
api_call GET "/projects/$PROJECT_NAME/pipelines" | jq '.data[:3] // .'
}
verify_patterns() {
print_header "Verifying Patterns: $PROJECT_NAME"
verify_chassis_patterns "$PROJECT_NAME"
verify_design_system "$PROJECT_NAME"
verify_service_patterns "$PROJECT_NAME"
verify_nextjs_app "$PROJECT_NAME"
print_header "Verification Complete"
}
teardown() {
print_header "Tearing down: $PROJECT_NAME"
local result
result=$(api_call DELETE "/project/$PROJECT_NAME")
echo "$result" | jq '.'
echo ""
print_success "Project deleted. Gitea repo preserved."
}
case "$COMMAND" in
run)
run_flow
;;
status)
check_status
;;
verify-patterns)
verify_patterns
;;
teardown)
teardown
;;
*)
echo "Unknown command: $COMMAND"
echo "Valid commands: run, status, verify-patterns, teardown"
exit 1
;;
esac