commit d92b6c4698506f6d81e98ec499b7b913454f0129 Author: Zhongwei Li Date: Sat Nov 29 18:52:21 2025 +0800 Initial commit diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..4b8d99a --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,15 @@ +{ + "name": "api-load-tester", + "description": "Load test APIs with k6, Gatling, or Artillery", + "version": "1.0.0", + "author": { + "name": "Jeremy Longshore", + "email": "[email protected]" + }, + "skills": [ + "./skills" + ], + "commands": [ + "./commands" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..8685a64 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# api-load-tester + +Load test APIs with k6, Gatling, or Artillery diff --git a/commands/run-load-test.md b/commands/run-load-test.md new file mode 100644 index 0000000..e660809 --- /dev/null +++ b/commands/run-load-test.md @@ -0,0 +1,987 @@ +--- +description: Run API load tests with k6, Artillery, or Gatling to measure performance under load +shortcut: loadtest +--- + +# Run API Load Test + +Execute comprehensive load tests to measure API performance, identify bottlenecks, and validate scalability under realistic traffic patterns. + +## Design Decisions + +This command supports multiple load testing tools to accommodate different testing scenarios and team preferences: + +- **k6**: Chosen for developer-friendly JavaScript API, excellent CLI output, and built-in metrics +- **Artillery**: Selected for YAML configuration simplicity and scenario-based testing +- **Gatling**: Included for enterprise-grade reporting and Scala DSL power users + +Alternative approaches considered: +- **JMeter**: Excluded due to GUI-heavy approach and XML configuration complexity +- **Locust**: Considered but not included to limit Python dependencies +- **Custom solutions**: Avoided to leverage battle-tested tools with proven metrics accuracy + +## When to Use This Command + +**USE WHEN:** +- Validating API performance before production deployment +- Establishing baseline performance metrics for SLAs +- Testing autoscaling behavior under load +- Identifying memory leaks or resource exhaustion issues +- Comparing performance across API versions +- Simulating Black Friday or high-traffic events + +**DON'T USE WHEN:** +- Testing production APIs without permission (use staging environments) +- You need functional correctness testing (use integration tests instead) +- Testing third-party APIs you don't control +- During active development (use unit/integration tests first) + +## Prerequisites + +**Required:** +- Node.js 18+ (for k6 and Artillery) +- Java 11+ (for Gatling) +- Target API endpoint accessible from your machine +- API authentication credentials (if required) + +**Recommended:** +- Monitoring tools configured (Prometheus, Grafana, DataDog) +- Baseline metrics from previous test runs +- Staging environment that mirrors production capacity + +**Install Tools:** +```bash +# k6 (recommended for most use cases) +brew install k6 # macOS +sudo apt-get install k6 # Ubuntu + +# Artillery +npm install -g artillery + +# Gatling +wget https://repo1.maven.org/maven2/io/gatling/highcharts/gatling-charts-highcharts-bundle/3.9.5/gatling-charts-highcharts-bundle-3.9.5.zip +unzip gatling-charts-highcharts-bundle-3.9.5.zip +``` + +## Detailed Process + +### Step 1: Define Test Objectives +Establish clear performance targets before running tests: +- **Response time**: p95 < 200ms, p99 < 500ms +- **Throughput**: 1000 requests/second sustained +- **Error rate**: < 0.1% under normal load +- **Concurrent users**: Support 500 simultaneous users + +Document expected behavior under different load levels: +- Normal load: 100-500 RPS +- Peak load: 1000-2000 RPS +- Stress test: 3000+ RPS until failure + +### Step 2: Configure Test Scenario +Create test scripts matching realistic user behavior patterns: + +**k6 test script** (`load-test.js`): +```javascript +import http from 'k6/http'; +import { check, sleep } from 'k6'; + +export const options = { + stages: [ + { duration: '2m', target: 100 }, // Ramp-up + { duration: '5m', target: 100 }, // Sustained load + { duration: '2m', target: 200 }, // Scale up + { duration: '5m', target: 200 }, // Sustained peak + { duration: '2m', target: 0 }, // Ramp-down + ], + thresholds: { + http_req_duration: ['p(95)<200', 'p(99)<500'], + http_req_failed: ['rate<0.01'], + }, +}; + +export default function () { + const res = http.get('https://api.example.com/v1/products'); + check(res, { + 'status is 200': (r) => r.status === 200, + 'response time < 200ms': (r) => r.timings.duration < 200, + }); + sleep(1); +} +``` + +**Artillery config** (`artillery.yml`): +```yaml +config: + target: 'https://api.example.com' + phases: + - duration: 60 + arrivalRate: 10 + name: "Warm up" + - duration: 300 + arrivalRate: 50 + name: "Sustained load" + - duration: 120 + arrivalRate: 100 + name: "Peak load" + processor: "./flows.js" +scenarios: + - name: "Product browsing flow" + flow: + - get: + url: "/v1/products" + capture: + - json: "$.products[0].id" + as: "productId" + - get: + url: "/v1/products/{{ productId }}" + - think: 3 +``` + +### Step 3: Execute Load Test +Run tests with appropriate parameters and monitor system resources: + +```bash +# k6 test execution with custom parameters +k6 run load-test.js \ + --vus 100 \ + --duration 10m \ + --out json=results.json \ + --summary-export=summary.json + +# Artillery with real-time reporting +artillery run artillery.yml \ + --output report.json + +# Gatling test execution +./gatling.sh -s com.example.LoadTest \ + -rf results/ +``` + +Monitor system metrics during execution: +- CPU utilization (should stay below 80%) +- Memory consumption (watch for leaks) +- Network I/O (bandwidth saturation) +- Database connections (connection pool exhaustion) + +### Step 4: Analyze Results +Review metrics to identify performance bottlenecks: + +**Response Time Analysis:** +```bash +# k6 summary shows percentile distribution + http_req_duration..............: avg=156ms p(95)=289ms p(99)=456ms + http_req_failed................: 0.12% (12 failures / 10000 requests) + http_reqs......................: 10000 166.67/s + vus............................: 100 min=0 max=100 +``` + +Key metrics to examine: +- **p50 (median)**: Typical user experience +- **p95**: Worst case for 95% of users +- **p99**: Tail latency affecting 1% of requests +- **Error rate**: Percentage of failed requests +- **Throughput**: Successful requests per second + +### Step 5: Generate Reports and Recommendations +Create actionable reports with findings and optimization suggestions: + +**Performance Report Structure:** +```markdown +# Load Test Results - 2025-10-11 + +## Test Configuration +- Duration: 10 minutes +- Virtual Users: 100 +- Target: https://api.example.com/v1/products + +## Results Summary +- Total Requests: 10,000 +- Success Rate: 99.88% +- Avg Response Time: 156ms +- p95 Response Time: 289ms +- Throughput: 166.67 RPS + +## Findings +1. Database query optimization needed (p99 spikes to 456ms) +2. Connection pool exhausted at 150 concurrent users +3. Memory leak detected after 8 minutes + +## Recommendations +1. Add database indexes on product_id and category +2. Increase connection pool from 20 to 50 +3. Fix memory leak in image processing service +``` + +## Output Format + +The command generates structured performance reports: + +**Console Output:** +``` +Running load test with k6... + + execution: local + script: load-test.js + output: json (results.json) + + scenarios: (100.00%) 1 scenario, 200 max VUs, 17m0s max duration + + data_received..................: 48 MB 80 kB/s + data_sent......................: 2.4 MB 4.0 kB/s + http_req_blocked...............: avg=1.23ms p(95)=3.45ms p(99)=8.91ms + http_req_connecting............: avg=856µs p(95)=2.34ms p(99)=5.67ms + http_req_duration..............: avg=156.78ms p(95)=289.45ms p(99)=456.12ms + http_req_failed................: 0.12% + http_req_receiving.............: avg=234µs p(95)=567µs p(99)=1.23ms + http_req_sending...............: avg=123µs p(95)=345µs p(99)=789µs + http_req_tls_handshaking.......: avg=0s p(95)=0s p(99)=0s + http_req_waiting...............: avg=156.42ms p(95)=288.89ms p(99)=455.34ms + http_reqs......................: 10000 166.67/s + iteration_duration.............: avg=1.16s p(95)=1.29s p(99)=1.46s + iterations.....................: 10000 166.67/s + vus............................: 100 min=0 max=200 + vus_max........................: 200 min=200 max=200 +``` + +**JSON Report:** +```json +{ + "metrics": { + "http_req_duration": { + "avg": 156.78, + "p95": 289.45, + "p99": 456.12 + }, + "http_req_failed": 0.0012, + "http_reqs": { + "count": 10000, + "rate": 166.67 + } + }, + "root_group": { + "checks": { + "status is 200": { + "passes": 9988, + "fails": 12 + } + } + } +} +``` + +## Code Examples + +### Example 1: Basic Load Test with k6 + +Test a REST API endpoint with gradual ramp-up and threshold validation: + +```javascript +// basic-load-test.js +import http from 'k6/http'; +import { check, sleep } from 'k6'; +import { Rate } from 'k6/metrics'; + +// Custom metrics +const errorRate = new Rate('errors'); + +export const options = { + // Ramp-up pattern: 0 -> 50 -> 100 -> 50 -> 0 + stages: [ + { duration: '1m', target: 50 }, // Ramp-up to 50 users + { duration: '3m', target: 50 }, // Stay at 50 users + { duration: '1m', target: 100 }, // Spike to 100 users + { duration: '3m', target: 100 }, // Stay at 100 users + { duration: '1m', target: 50 }, // Scale down to 50 + { duration: '1m', target: 0 }, // Ramp-down to 0 + ], + + // Performance thresholds (test fails if exceeded) + thresholds: { + 'http_req_duration': ['p(95)<300', 'p(99)<500'], + 'http_req_failed': ['rate<0.01'], // Less than 1% errors + 'errors': ['rate<0.1'], + }, +}; + +export default function () { + // Test parameters + const baseUrl = 'https://api.example.com'; + const params = { + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${__ENV.API_TOKEN}`, + }, + }; + + // API request + const res = http.get(`${baseUrl}/v1/products?limit=20`, params); + + // Validation checks + const checkRes = check(res, { + 'status is 200': (r) => r.status === 200, + 'response time < 300ms': (r) => r.timings.duration < 300, + 'has products': (r) => r.json('products').length > 0, + 'valid JSON': (r) => { + try { + JSON.parse(r.body); + return true; + } catch (e) { + return false; + } + }, + }); + + // Track custom error metric + errorRate.add(!checkRes); + + // Simulate user think time + sleep(Math.random() * 3 + 1); // 1-4 seconds +} + +// Teardown function (runs once at end) +export function teardown(data) { + console.log('Load test completed'); +} +``` + +**Run command:** +```bash +# Set API token and execute +export API_TOKEN="your-token-here" +k6 run basic-load-test.js \ + --out json=results.json \ + --summary-export=summary.json + +# Generate HTML report from JSON +k6-reporter results.json --output report.html +``` + +### Example 2: Stress Testing with Artillery + +Test API breaking point with gradual load increase until failure: + +```yaml +# stress-test.yml +config: + target: 'https://api.example.com' + phases: + # Gradual ramp-up to find breaking point + - duration: 60 + arrivalRate: 10 + name: "Phase 1: Baseline (10 RPS)" + - duration: 60 + arrivalRate: 50 + name: "Phase 2: Moderate (50 RPS)" + - duration: 60 + arrivalRate: 100 + name: "Phase 3: High (100 RPS)" + - duration: 60 + arrivalRate: 200 + name: "Phase 4: Stress (200 RPS)" + - duration: 60 + arrivalRate: 400 + name: "Phase 5: Breaking point (400 RPS)" + + # Environment variables + variables: + api_token: "{{ $processEnvironment.API_TOKEN }}" + + # HTTP settings + http: + timeout: 10 + pool: 50 + + # Custom plugins + plugins: + expect: {} + metrics-by-endpoint: {} + + # Success criteria + ensure: + p95: 500 + p99: 1000 + maxErrorRate: 1 + +# Test scenarios +scenarios: + - name: "Product CRUD operations" + weight: 70 + flow: + # List products + - get: + url: "/v1/products" + headers: + Authorization: "Bearer {{ api_token }}" + expect: + - statusCode: 200 + - contentType: json + - hasProperty: products + capture: + - json: "$.products[0].id" + as: "productId" + + # Get product details + - get: + url: "/v1/products/{{ productId }}" + headers: + Authorization: "Bearer {{ api_token }}" + expect: + - statusCode: 200 + - hasProperty: id + + # Think time (user reading) + - think: 2 + + # Search products + - get: + url: "/v1/products/search?q=laptop" + headers: + Authorization: "Bearer {{ api_token }}" + expect: + - statusCode: 200 + + - name: "User authentication flow" + weight: 20 + flow: + - post: + url: "/v1/auth/login" + json: + email: "test@example.com" + password: "password123" + expect: + - statusCode: 200 + - hasProperty: token + capture: + - json: "$.token" + as: "userToken" + + - get: + url: "/v1/users/me" + headers: + Authorization: "Bearer {{ userToken }}" + expect: + - statusCode: 200 + + - name: "Shopping cart operations" + weight: 10 + flow: + - post: + url: "/v1/cart/items" + headers: + Authorization: "Bearer {{ api_token }}" + json: + productId: "{{ productId }}" + quantity: 1 + expect: + - statusCode: 201 + + - get: + url: "/v1/cart" + headers: + Authorization: "Bearer {{ api_token }}" + expect: + - statusCode: 200 + - hasProperty: items +``` + +**Run with custom processor:** +```javascript +// flows.js - Custom logic for Artillery +module.exports = { + // Before request hook + setAuthToken: function(requestParams, context, ee, next) { + requestParams.headers = requestParams.headers || {}; + requestParams.headers['X-Request-ID'] = `req-${Date.now()}-${Math.random()}`; + return next(); + }, + + // After response hook + logResponse: function(requestParams, response, context, ee, next) { + if (response.statusCode >= 400) { + console.log(`Error: ${response.statusCode} - ${requestParams.url}`); + } + return next(); + }, + + // Custom function to generate dynamic data + generateTestData: function(context, events, done) { + context.vars.userId = `user-${Math.floor(Math.random() * 10000)}`; + context.vars.timestamp = new Date().toISOString(); + return done(); + } +}; +``` + +**Execute stress test:** +```bash +# Run with environment variable +API_TOKEN="your-token" artillery run stress-test.yml \ + --output stress-results.json + +# Generate HTML report +artillery report stress-results.json \ + --output stress-report.html + +# Run with custom config overrides +artillery run stress-test.yml \ + --config config.phases[0].duration=30 \ + --config config.phases[0].arrivalRate=20 +``` + +### Example 3: Performance Testing with Gatling (Scala DSL) + +Enterprise-grade load test with complex scenarios and detailed reporting: + +```scala +// LoadSimulation.scala +package com.example.loadtest + +import io.gatling.core.Predef._ +import io.gatling.http.Predef._ +import scala.concurrent.duration._ + +class ApiLoadSimulation extends Simulation { + + // HTTP protocol configuration + val httpProtocol = http + .baseUrl("https://api.example.com") + .acceptHeader("application/json") + .authorizationHeader("Bearer ${accessToken}") + .userAgentHeader("Gatling Load Test") + .shareConnections + + // Feeders for test data + val userFeeder = csv("users.csv").circular + val productFeeder = csv("products.csv").random + + // Custom headers + val sentHeaders = Map( + "X-Request-ID" -> "${requestId}", + "X-Client-Version" -> "1.0.0" + ) + + // Scenario 1: Browse products + val browseProducts = scenario("Browse Products") + .feed(userFeeder) + .exec(session => session.set("requestId", java.util.UUID.randomUUID.toString)) + .exec( + http("List Products") + .get("/v1/products") + .headers(sentHeaders) + .check(status.is(200)) + .check(jsonPath("$.products[*].id").findAll.saveAs("productIds")) + ) + .pause(2, 5) + .exec( + http("Get Product Details") + .get("/v1/products/${productIds.random()}") + .check(status.is(200)) + .check(jsonPath("$.id").exists) + .check(jsonPath("$.price").ofType[Double].saveAs("price")) + ) + .pause(1, 3) + + // Scenario 2: Search and filter + val searchProducts = scenario("Search Products") + .exec(session => session.set("requestId", java.util.UUID.randomUUID.toString)) + .exec( + http("Search Products") + .get("/v1/products/search") + .queryParam("q", "laptop") + .queryParam("minPrice", "500") + .queryParam("maxPrice", "2000") + .headers(sentHeaders) + .check(status.is(200)) + .check(jsonPath("$.total").ofType[Int].gt(0)) + ) + .pause(2, 4) + .exec( + http("Apply Filters") + .get("/v1/products/search") + .queryParam("q", "laptop") + .queryParam("brand", "Dell") + .queryParam("sort", "price") + .check(status.is(200)) + ) + + // Scenario 3: Checkout flow + val checkout = scenario("Checkout Flow") + .feed(userFeeder) + .feed(productFeeder) + .exec(session => session.set("requestId", java.util.UUID.randomUUID.toString)) + .exec( + http("Add to Cart") + .post("/v1/cart/items") + .headers(sentHeaders) + .body(StringBody("""{"productId": "${productId}", "quantity": 1}""")) + .asJson + .check(status.is(201)) + .check(jsonPath("$.cartId").saveAs("cartId")) + ) + .pause(1, 2) + .exec( + http("Get Cart") + .get("/v1/cart/${cartId}") + .check(status.is(200)) + .check(jsonPath("$.total").ofType[Double].saveAs("total")) + ) + .pause(2, 4) + .exec( + http("Create Order") + .post("/v1/orders") + .body(StringBody("""{"cartId": "${cartId}", "paymentMethod": "credit_card"}""")) + .asJson + .check(status.in(200, 201)) + .check(jsonPath("$.orderId").saveAs("orderId")) + ) + .exec( + http("Get Order Status") + .get("/v1/orders/${orderId}") + .check(status.is(200)) + .check(jsonPath("$.status").is("pending")) + ) + + // Load profile: Realistic production traffic pattern + setUp( + // 70% users browse products + browseProducts.inject( + rampUsersPerSec(1) to 50 during (2 minutes), + constantUsersPerSec(50) during (5 minutes), + rampUsersPerSec(50) to 100 during (3 minutes), + constantUsersPerSec(100) during (5 minutes), + rampUsersPerSec(100) to 0 during (2 minutes) + ).protocols(httpProtocol), + + // 20% users search + searchProducts.inject( + rampUsersPerSec(1) to 15 during (2 minutes), + constantUsersPerSec(15) during (10 minutes), + rampUsersPerSec(15) to 0 during (2 minutes) + ).protocols(httpProtocol), + + // 10% users complete checkout + checkout.inject( + rampUsersPerSec(1) to 10 during (3 minutes), + constantUsersPerSec(10) during (10 minutes), + rampUsersPerSec(10) to 0 during (2 minutes) + ).protocols(httpProtocol) + ).protocols(httpProtocol) + .assertions( + global.responseTime.max.lt(2000), + global.responseTime.percentile3.lt(500), + global.successfulRequests.percent.gt(99) + ) +} +``` + +**Supporting data files:** + +`users.csv`: +```csv +userId,accessToken +user-001,eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... +user-002,eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... +user-003,eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... +``` + +`products.csv`: +```csv +productId,category +prod-001,electronics +prod-002,clothing +prod-003,books +``` + +**Run Gatling simulation:** +```bash +# Using Gatling Maven plugin +mvn gatling:test -Dgatling.simulationClass=com.example.loadtest.ApiLoadSimulation + +# Using standalone Gatling +./gatling.sh -s com.example.loadtest.ApiLoadSimulation \ + -rf results/ + +# Generate report only (from previous run) +./gatling.sh -ro results/apisimulation-20251011143022456 +``` + +**Gatling configuration** (`gatling.conf`): +```hocon +gatling { + core { + outputDirectoryBaseName = "api-load-test" + runDescription = "Production load simulation" + encoding = "utf-8" + simulationClass = "" + } + charting { + indicators { + lowerBound = 100 # Lower bound for response time (ms) + higherBound = 500 # Higher bound for response time (ms) + percentile1 = 50 # First percentile + percentile2 = 75 # Second percentile + percentile3 = 95 # Third percentile + percentile4 = 99 # Fourth percentile + } + } + http { + ahc { + pooledConnectionIdleTimeout = 60000 + readTimeout = 60000 + requestTimeout = 60000 + connectionTimeout = 30000 + maxConnections = 200 + maxConnectionsPerHost = 50 + } + } + data { + writers = [console, file] + } +} +``` + +## Error Handling + +Common errors and solutions: + +**Connection Refused:** +``` +Error: connect ECONNREFUSED 127.0.0.1:8080 +``` +Solution: Verify API is running and accessible. Check network connectivity and firewall rules. + +**Timeout Errors:** +``` +http_req_failed: 45.2% (4520 failures / 10000 requests) +``` +Solution: Increase timeout values or reduce concurrent users. API may be overwhelmed. + +**SSL/TLS Errors:** +``` +Error: x509: certificate signed by unknown authority +``` +Solution: Add `insecureSkipTLSVerify: true` or configure proper CA certificates. + +**Rate Limiting:** +``` +HTTP 429 Too Many Requests +``` +Solution: Reduce request rate or increase rate limits on API server. Add backoff logic. + +**Memory Exhaustion:** +``` +JavaScript heap out of memory +``` +Solution: Increase Node.js memory limit: `NODE_OPTIONS=--max-old-space-size=4096 k6 run test.js` + +**Authentication Failures:** +``` +HTTP 401 Unauthorized +``` +Solution: Verify API tokens are valid and not expired. Check authorization headers. + +## Configuration Options + +### k6 Options + +```bash +--vus N # Number of virtual users (default: 1) +--duration Xm # Test duration (e.g., 10m, 30s) +--iterations N # Total iterations across all VUs +--stage "Xm:N" # Add load stage (duration:target) +--rps N # Max requests per second +--max-redirects N # Max HTTP redirects (default: 10) +--batch N # Max parallel batch requests +--batch-per-host N # Max parallel requests per host +--http-debug # Enable HTTP debug logging +--no-connection-reuse # Disable HTTP keep-alive +--throw # Throw errors on failed HTTP requests +--summary-trend-stats # Custom summary stats (e.g., "avg,p(95),p(99)") +--out json=file.json # Export results to JSON +--out influxdb=http://... # Export to InfluxDB +--out statsd # Export to StatsD +``` + +### Artillery Options + +```bash +--target URL # Override target URL +--output FILE # Save results to JSON file +--overrides FILE # Override config with JSON file +--variables FILE # Load variables from JSON +--config KEY=VALUE # Override single config value +--environment ENV # Select environment from config +--solo # Run test without publishing +--quiet # Suppress output +--plugins # List installed plugins +--dotenv FILE # Load environment from .env file +``` + +### Gatling Options + +```bash +-s CLASS # Simulation class to run +-rf FOLDER # Results folder +-rd DESC # Run description +-nr # No reports generation +-ro FOLDER # Generate reports only +``` + +## Best Practices + +### DO: +- Start with baseline test (low load) to verify test scripts work correctly +- Ramp up load gradually to identify inflection points +- Monitor backend resources (CPU, memory, database) during tests +- Use realistic think times (1-5 seconds) to simulate user behavior +- Test in staging environment that mirrors production capacity +- Run tests multiple times to establish consistency +- Document test configuration and results for historical comparison +- Use connection pooling and HTTP keep-alive for realistic scenarios +- Set appropriate timeouts (30-60 seconds for most APIs) +- Clean up test data after runs (especially for write-heavy tests) + +### DON'T: +- Don't load test production without explicit permission and monitoring +- Don't ignore warmup period (JIT compilation, cache warming) +- Don't test from same datacenter as API (unrealistic latency) +- Don't use default test data (create realistic, varied datasets) +- Don't skip cool-down period (observe resource cleanup) +- Don't test only happy paths (include error scenarios) +- Don't ignore database connection limits +- Don't run tests during production deployments +- Don't compare results across different network conditions +- Don't test third-party APIs without permission + +### TIPS: +- Use distributed load generation for tests > 1000 VUs +- Export metrics to monitoring systems (Prometheus, DataDog) for correlation +- Create custom dashboards showing load test progress in real-time +- Use percentiles (p95, p99) instead of averages for SLA targets +- Test cache warm vs cold scenarios separately +- Include authentication overhead in realistic flows +- Validate response bodies, not just status codes +- Use unique IDs per virtual user to avoid data conflicts +- Schedule tests during low-traffic periods +- Keep test scripts in version control with API code + +## Related Commands + +- `/api-mock-server` - Create mock API for testing without backend +- `/api-monitoring-dashboard` - Set up real-time monitoring during load tests +- `/api-cache-manager` - Configure caching to improve performance under load +- `/api-rate-limiter` - Implement rate limiting to protect APIs +- `/deployment-pipeline-orchestrator` - Integrate load tests into CI/CD pipeline +- `/kubernetes-deployment-creator` - Configure autoscaling based on load test findings + +## Performance Considerations + +### Test Environment Sizing +- **Client machine**: 1 VU ≈ 1-10 MB RAM, 0.01-0.1 CPU cores +- **Network bandwidth**: 1000 VUs ≈ 10-100 Mbps depending on payload size +- **k6 limits**: Single instance handles 30,000-40,000 VUs (depends on script complexity) +- **Artillery limits**: Single instance handles 5,000-10,000 RPS +- **Gatling limits**: Single instance handles 50,000+ VUs (JVM-based) + +### Backend Resource Planning +- **Database connections**: Plan for peak concurrent users + connection pool overhead +- **CPU utilization**: Keep below 80% under sustained load (leave headroom for spikes) +- **Memory**: Monitor for leaks (heap should stabilize after warmup) +- **Network I/O**: Ensure network bandwidth exceeds expected throughput by 50% + +### Optimization Strategies +- **HTTP keep-alive**: Reduces connection overhead by 50-80% +- **Response compression**: Reduces bandwidth by 60-80% for text responses +- **CDN caching**: Offloads 70-90% of static asset requests +- **Database indexing**: Can improve query performance by 10-100x +- **Connection pooling**: Reduces latency by 20-50ms per request + +## Security Notes + +### Testing Permissions +- Obtain written approval before load testing any environment +- Verify testing is allowed by API terms of service +- Use dedicated test accounts with limited privileges +- Test in isolated environments to prevent data corruption + +### Credential Management +- Never hardcode API keys or passwords in test scripts +- Use environment variables: `export API_TOKEN=$(vault read -field=token secret/api)` +- Rotate test credentials regularly +- Use short-lived tokens (JWT with 1-hour expiry) +- Store sensitive data in secrets managers (Vault, AWS Secrets Manager) + +### Data Privacy +- Use synthetic test data (never real customer PII) +- Anonymize logs and results before sharing +- Clean up test data immediately after test completion +- Encrypt results files containing sensitive information + +### Network Security +- Run tests from trusted networks (avoid public WiFi) +- Use VPN when testing internal APIs +- Implement IP whitelisting for test traffic +- Monitor for anomalous traffic patterns during tests + +## Troubleshooting Guide + +### Issue: Inconsistent Results Between Runs +**Symptoms:** Response times vary by > 50% between identical test runs +**Diagnosis:** +- Check for background jobs or cron tasks running during test +- Verify database wasn't backed up during test +- Ensure no other load tests running concurrently +**Solution:** +- Schedule tests during known quiet periods +- Disable background tasks during test window +- Run multiple iterations and take median results + +### Issue: Low Throughput Despite Low CPU/Memory +**Symptoms:** API handling only 100 RPS despite 20% CPU usage +**Diagnosis:** +- Check network bandwidth utilization +- Examine database connection pool exhaustion +- Look for synchronous I/O blocking (file system, external API calls) +**Solution:** +- Increase connection pool size +- Implement async I/O for external calls +- Add caching layer (Redis) for frequently accessed data + +### Issue: Error Rate Increases Under Load +**Symptoms:** 0.1% errors at 100 RPS, 5% errors at 500 RPS +**Diagnosis:** +- Database deadlocks or lock contention +- Race conditions in concurrent code paths +- Resource exhaustion (file descriptors, sockets) +**Solution:** +- Add database query logging to identify slow queries +- Implement optimistic locking or queue-based processing +- Increase file descriptor limits: `ulimit -n 65536` + +### Issue: Memory Leak Detected +**Symptoms:** Memory usage grows continuously without stabilizing +**Diagnosis:** +- Heap dump analysis shows growing object count +- GC frequency increases over time +- API becomes unresponsive after extended load +**Solution:** +- Profile application with heap analyzer (Chrome DevTools, VisualVM) +- Check for unclosed database connections or file handles +- Review event listener registration (potential memory leak source) + +### Issue: Test Client Crashes +**Symptoms:** k6/Artillery process terminated with OOM error +**Diagnosis:** +- Too many VUs for available client memory +- Large response bodies consuming memory +- Results export causing memory pressure +**Solution:** +- Reduce VU count or distribute across multiple machines +- Increase Node.js memory: `NODE_OPTIONS=--max-old-space-size=8192` +- Disable detailed logging: `--quiet` or `--summary-export` only + +## Version History + +- **1.0.0** (2025-10-11) - Initial release with k6, Artillery, and Gatling support +- **1.1.0** (2025-10-15) - Added custom metrics and Prometheus integration +- **1.2.0** (2025-10-20) - Distributed load testing support for high-scale scenarios diff --git a/plugin.lock.json b/plugin.lock.json new file mode 100644 index 0000000..479bf7f --- /dev/null +++ b/plugin.lock.json @@ -0,0 +1,101 @@ +{ + "$schema": "internal://schemas/plugin.lock.v1.json", + "pluginId": "gh:jeremylongshore/claude-code-plugins-plus:plugins/api-development/api-load-tester", + "normalized": { + "repo": null, + "ref": "refs/tags/v20251128.0", + "commit": "6b4ffa030b1b839ade51b4a1f5762f6e420fc0f5", + "treeHash": "bc0630300ff6ef2d77ce7d8d23e43ebd1db89403d359c12832f58b5b324fc3dd", + "generatedAt": "2025-11-28T10:18:06.877388Z", + "toolVersion": "publish_plugins.py@0.2.0" + }, + "origin": { + "remote": "git@github.com:zhongweili/42plugin-data.git", + "branch": "master", + "commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390", + "repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data" + }, + "manifest": { + "name": "api-load-tester", + "description": "Load test APIs with k6, Gatling, or Artillery", + "version": "1.0.0" + }, + "content": { + "files": [ + { + "path": "README.md", + "sha256": "b8b2a07a126dca5da8b20c95583c9daf3a59e3ee9931700c349ac5d32935542b" + }, + { + "path": ".claude-plugin/plugin.json", + "sha256": "910fbcd39f613332874c52fdf9b6dea2f924b5000c475d4333c9ea0b6e394473" + }, + { + "path": "commands/run-load-test.md", + "sha256": "5d0c1b06cdb734148da23b52084b8efd3ee1da7305861a1e80f0d455598cb9d1" + }, + { + "path": "skills/skill-adapter/references/examples.md", + "sha256": "922bbc3c4ebf38b76f515b5c1998ebde6bf902233e00e2c5a0e9176f975a7572" + }, + { + "path": "skills/skill-adapter/references/best-practices.md", + "sha256": "c8f32b3566252f50daacd346d7045a1060c718ef5cfb07c55a0f2dec5f1fb39e" + }, + { + "path": "skills/skill-adapter/references/README.md", + "sha256": "ccbb491ac63a6d9d5bf3f5cab07c46ea7399fae2464d8062d8f6ce189c3ce0f2" + }, + { + "path": "skills/skill-adapter/scripts/helper-template.sh", + "sha256": "0881d5660a8a7045550d09ae0acc15642c24b70de6f08808120f47f86ccdf077" + }, + { + "path": "skills/skill-adapter/scripts/validation.sh", + "sha256": "92551a29a7f512d2036e4f1fb46c2a3dc6bff0f7dde4a9f699533e446db48502" + }, + { + "path": "skills/skill-adapter/scripts/README.md", + "sha256": "3030e09cd28ef976fb9667d9cc01a15ac343fb4202c7e7a353a76162e3e22ce5" + }, + { + "path": "skills/skill-adapter/assets/example_api_spec.json", + "sha256": "ea21efae7e895abd63c09ea55a80c301fb50e4b0af4f5d69cf4c83ab0fa2cc06" + }, + { + "path": "skills/skill-adapter/assets/test-data.json", + "sha256": "ac17dca3d6e253a5f39f2a2f1b388e5146043756b05d9ce7ac53a0042eee139d" + }, + { + "path": "skills/skill-adapter/assets/k6_template.js", + "sha256": "cac5f04176b8e146c62c4629db35085a17f8c786048be2f1330d4e30f2ed1a30" + }, + { + "path": "skills/skill-adapter/assets/README.md", + "sha256": "154f460989704812115099f601d330b07c1b9f7b3127b7aae5c3ea8cb4067cb4" + }, + { + "path": "skills/skill-adapter/assets/skill-schema.json", + "sha256": "f5639ba823a24c9ac4fb21444c0717b7aefde1a4993682897f5bf544f863c2cd" + }, + { + "path": "skills/skill-adapter/assets/artillery_template.yaml", + "sha256": "5122fa46a7afdebc973dd654382bf664decee644042e49c7f7c2574d88890627" + }, + { + "path": "skills/skill-adapter/assets/gatling_template.scala", + "sha256": "bd3935dfd3fe4ccfd25f39b7da55fca2993380b28390788f6930b7aa10fb3264" + }, + { + "path": "skills/skill-adapter/assets/config-template.json", + "sha256": "0c2ba33d2d3c5ccb266c0848fc43caa68a2aa6a80ff315d4b378352711f83e1c" + } + ], + "dirSha256": "bc0630300ff6ef2d77ce7d8d23e43ebd1db89403d359c12832f58b5b324fc3dd" + }, + "security": { + "scannedAt": null, + "scannerVersion": null, + "flags": [] + } +} \ No newline at end of file diff --git a/skills/skill-adapter/assets/README.md b/skills/skill-adapter/assets/README.md new file mode 100644 index 0000000..85af5d0 --- /dev/null +++ b/skills/skill-adapter/assets/README.md @@ -0,0 +1,8 @@ +# Assets + +Bundled resources for api-load-tester skill + +- [ ] k6_template.js: Template for k6 load test scripts. +- [ ] gatling_template.scala: Template for Gatling load test scripts. +- [ ] artillery_template.yaml: Template for Artillery load test scripts. +- [ ] example_api_spec.json: Example API specification for load testing. diff --git a/skills/skill-adapter/assets/artillery_template.yaml b/skills/skill-adapter/assets/artillery_template.yaml new file mode 100644 index 0000000..19e69fa --- /dev/null +++ b/skills/skill-adapter/assets/artillery_template.yaml @@ -0,0 +1,43 @@ +# Artillery Load Test Configuration + +config: + target: "REPLACE_ME" # URL of the API endpoint to test + phases: + - duration: 60 # Ramp up duration in seconds + arrivalRate: 10 # Number of virtual users to start per second + name: "Ramp Up" + - duration: 300 # Sustain duration in seconds + arrivalRate: 10 # Number of virtual users to maintain + name: "Sustain Load" + defaults: + headers: + Content-Type: "application/json" + #Add any default headers here. Authorization tokens, etc. + #Authorization: "Bearer YOUR_AUTH_TOKEN" + +scenarios: + - name: "Basic API Request" + flow: + - get: + url: "/YOUR_ENDPOINT_HERE" # Replace with your API endpoint + # Capture response data to variables + capture: + - json: "$.id" + as: "id" + # Example of using a custom function + # beforeRequest: "myFunction" + - think: 1 # Pause for 1 second between requests + - log: "Request completed. ID: {{ id }}" # Log the captured ID + +# Define custom functions (optional, define in separate .js file referenced in config) +# functions: "./my_functions.js" + +# Example function (in my_functions.js) +# module.exports.myFunction = function(requestParams, context, ee, next) { +# // Add custom logic here +# return next(); +# }; + +# Variables (can be loaded from CSV or defined inline) +# variables: +# - id: [1, 2, 3, 4, 5] \ No newline at end of file diff --git a/skills/skill-adapter/assets/config-template.json b/skills/skill-adapter/assets/config-template.json new file mode 100644 index 0000000..16f1712 --- /dev/null +++ b/skills/skill-adapter/assets/config-template.json @@ -0,0 +1,32 @@ +{ + "skill": { + "name": "skill-name", + "version": "1.0.0", + "enabled": true, + "settings": { + "verbose": false, + "autoActivate": true, + "toolRestrictions": true + } + }, + "triggers": { + "keywords": [ + "example-trigger-1", + "example-trigger-2" + ], + "patterns": [] + }, + "tools": { + "allowed": [ + "Read", + "Grep", + "Bash" + ], + "restricted": [] + }, + "metadata": { + "author": "Plugin Author", + "category": "general", + "tags": [] + } +} diff --git a/skills/skill-adapter/assets/example_api_spec.json b/skills/skill-adapter/assets/example_api_spec.json new file mode 100644 index 0000000..a4d1d63 --- /dev/null +++ b/skills/skill-adapter/assets/example_api_spec.json @@ -0,0 +1,90 @@ +{ + "_comment": "Example API specification for load testing with k6, Gatling, or Artillery.", + "api_name": "Example API", + "description": "A simple API endpoint for demonstrating load testing configurations.", + "endpoints": [ + { + "name": "Get User", + "method": "GET", + "url": "https://example.com/api/users/123", + "_comment": "Example URL. Replace with your actual API endpoint.", + "headers": { + "Content-Type": "application/json", + "Authorization": "Bearer " + }, + "body": null, + "expected_status_code": 200, + "assertions": [ + { + "type": "json_path", + "path": "$.id", + "operator": "eq", + "value": 123, + "_comment": "Verify user ID is 123." + } + ] + }, + { + "name": "Create User", + "method": "POST", + "url": "https://example.com/api/users", + "headers": { + "Content-Type": "application/json" + }, + "body": { + "name": "John Doe", + "email": "john.doe@example.com" + }, + "expected_status_code": 201, + "assertions": [ + { + "type": "json_path", + "path": "$.name", + "operator": "eq", + "value": "John Doe", + "_comment": "Verify user name is John Doe." + } + ] + }, + { + "name": "Update User", + "method": "PUT", + "url": "https://example.com/api/users/123", + "headers": { + "Content-Type": "application/json" + }, + "body": { + "name": "Jane Doe", + "email": "jane.doe@example.com" + }, + "expected_status_code": 200, + "assertions": [ + { + "type": "json_path", + "path": "$.name", + "operator": "eq", + "value": "Jane Doe", + "_comment": "Verify user name is Jane Doe." + } + ] + }, + { + "name": "Delete User", + "method": "DELETE", + "url": "https://example.com/api/users/123", + "headers": {}, + "body": null, + "expected_status_code": 204, + "assertions": [] + } + ], + "load_profile": { + "tool": "k6", + "_comment": "Can be k6, gatling, or artillery", + "options": { + "vus": 10, + "duration": "10s", + "_comment": "k6 options, see k6 documentation for details" + } + } +} \ No newline at end of file diff --git a/skills/skill-adapter/assets/gatling_template.scala b/skills/skill-adapter/assets/gatling_template.scala new file mode 100644 index 0000000..3d807cd --- /dev/null +++ b/skills/skill-adapter/assets/gatling_template.scala @@ -0,0 +1,59 @@ +import scala.concurrent.duration._ + +import io.gatling.core.Predef._ +import io.gatling.http.Predef._ +import io.gatling.jdbc.Predef._ + +class APILoadTest extends Simulation { + + // --- Configuration --- + // Configure the target URL and request parameters here. + val baseURL = "${BASE_URL}" // e.g., "http://example.com" + val endpoint = "${ENDPOINT}" // e.g., "/api/resource" + val requestName = "Get Resource" // Descriptive name for the request + val requestBody = """${REQUEST_BODY}""" // JSON request body (optional) + + // --- HTTP Protocol Configuration --- + val httpProtocol = http + .baseUrl(baseURL) + .acceptHeader("application/json") + .contentTypeHeader("application/json") + // Add any other headers or configurations you need here. + // .userAgentHeader("Gatling/3.7.0") // Example User-Agent header + + // --- Scenario Definition --- + val scn = scenario("API Load Test Scenario") + .exec( + http(requestName) + .get(endpoint) // Or .post(endpoint).body(StringBody(requestBody)) if it's a POST request + //.headers(Map("Custom-Header" -> "value")) // Example custom header + .check(status.is(200)) // Validate the response status + // Add more checks as needed to validate the response data. + // .check(jsonPath("$.someField").is("expectedValue")) + ) + + // --- Simulation Setup --- + setUp( + scn.inject( + // Define your load profile here. Examples: + // - Constant load of 10 users per second for 30 seconds + // constantUsersPerSec(10).during(30.seconds), + + // - Ramp up from 1 to 10 users over 20 seconds + // rampUsersPerSec(1).to(10).during(20.seconds), + + // - Constant load for a period, then a ramp-up, then another constant load + // constantUsersPerSec(5).during(10.seconds), + // rampUsersPerSec(5).to(15).during(10.seconds), + // constantUsersPerSec(15).during(10.seconds) + + // Placeholders for easy adjustments. Replace these with your desired values. + constantUsersPerSec(${USERS_PER_SECOND}).during(${DURATION_SECONDS}.seconds) + ).protocols(httpProtocol) + ) + // Add global assertions if needed. For example: + // .assertions( + // global.responseTime.max.lt(1000), // Ensure maximum response time is less than 1000ms + // global.successfulRequests.percent.gt(95) // Ensure success rate is greater than 95% + // ) +} \ No newline at end of file diff --git a/skills/skill-adapter/assets/k6_template.js b/skills/skill-adapter/assets/k6_template.js new file mode 100644 index 0000000..43c80da --- /dev/null +++ b/skills/skill-adapter/assets/k6_template.js @@ -0,0 +1,52 @@ +// k6 load test script template + +import http from 'k6/http'; +import { check, sleep } from 'k6'; + +// Configuration options +export const options = { + stages: [ + { duration: '10s', target: 10 }, // Ramp up to 10 virtual users (VUs) over 10 seconds + { duration: '30s', target: 10 }, // Stay at 10 VUs for 30 seconds + { duration: '10s', target: 0 }, // Ramp down to 0 VUs over 10 seconds + ], + thresholds: { + http_req_duration: ['p95<500'], // 95% of requests must complete below 500ms + http_req_failed: ['rate<0.01'], // Error rate must be less than 1% + }, +}; + +// Virtual User (VU) function +export default function () { + // Replace with your API endpoint + const url = 'YOUR_API_ENDPOINT'; + + // Replace with your request body (if needed) + const payload = JSON.stringify({ + key1: 'value1', + key2: 'value2', + }); + + // Replace with your request headers (if needed) + const params = { + headers: { + 'Content-Type': 'application/json', + }, + }; + + // Make the HTTP request + const res = http.post(url, payload, params); + + // Check the response status code + check(res, { + 'is status 200': (r) => r.status === 200, + }); + + // Add more checks as needed (e.g., response body validation) + // check(res, { + // 'verify response': (r) => r.body.includes('expected_value'), + // }); + + // Sleep for a short duration between requests (adjust as needed) + sleep(1); +} \ No newline at end of file diff --git a/skills/skill-adapter/assets/skill-schema.json b/skills/skill-adapter/assets/skill-schema.json new file mode 100644 index 0000000..8dc154c --- /dev/null +++ b/skills/skill-adapter/assets/skill-schema.json @@ -0,0 +1,28 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Claude Skill Configuration", + "type": "object", + "required": ["name", "description"], + "properties": { + "name": { + "type": "string", + "pattern": "^[a-z0-9-]+$", + "maxLength": 64, + "description": "Skill identifier (lowercase, hyphens only)" + }, + "description": { + "type": "string", + "maxLength": 1024, + "description": "What the skill does and when to use it" + }, + "allowed-tools": { + "type": "string", + "description": "Comma-separated list of allowed tools" + }, + "version": { + "type": "string", + "pattern": "^\\d+\\.\\d+\\.\\d+$", + "description": "Semantic version (x.y.z)" + } + } +} diff --git a/skills/skill-adapter/assets/test-data.json b/skills/skill-adapter/assets/test-data.json new file mode 100644 index 0000000..f0cd871 --- /dev/null +++ b/skills/skill-adapter/assets/test-data.json @@ -0,0 +1,27 @@ +{ + "testCases": [ + { + "name": "Basic activation test", + "input": "trigger phrase example", + "expected": { + "activated": true, + "toolsUsed": ["Read", "Grep"], + "success": true + } + }, + { + "name": "Complex workflow test", + "input": "multi-step trigger example", + "expected": { + "activated": true, + "steps": 3, + "toolsUsed": ["Read", "Write", "Bash"], + "success": true + } + } + ], + "fixtures": { + "sampleInput": "example data", + "expectedOutput": "processed result" + } +} diff --git a/skills/skill-adapter/references/README.md b/skills/skill-adapter/references/README.md new file mode 100644 index 0000000..9fee520 --- /dev/null +++ b/skills/skill-adapter/references/README.md @@ -0,0 +1,8 @@ +# References + +Bundled resources for api-load-tester skill + +- [ ] k6_api_reference.md: API reference for k6 load testing tool. +- [ ] gatling_api_reference.md: API reference for Gatling load testing tool. +- [ ] artillery_api_reference.md: API reference for Artillery load testing tool. +- [ ] load_testing_best_practices.md: Best practices for API load testing. diff --git a/skills/skill-adapter/references/best-practices.md b/skills/skill-adapter/references/best-practices.md new file mode 100644 index 0000000..3505048 --- /dev/null +++ b/skills/skill-adapter/references/best-practices.md @@ -0,0 +1,69 @@ +# Skill Best Practices + +Guidelines for optimal skill usage and development. + +## For Users + +### Activation Best Practices + +1. **Use Clear Trigger Phrases** + - Match phrases from skill description + - Be specific about intent + - Provide necessary context + +2. **Provide Sufficient Context** + - Include relevant file paths + - Specify scope of analysis + - Mention any constraints + +3. **Understand Tool Permissions** + - Check allowed-tools in frontmatter + - Know what the skill can/cannot do + - Request appropriate actions + +### Workflow Optimization + +- Start with simple requests +- Build up to complex workflows +- Verify each step before proceeding +- Use skill consistently for related tasks + +## For Developers + +### Skill Development Guidelines + +1. **Clear Descriptions** + - Include explicit trigger phrases + - Document all capabilities + - Specify limitations + +2. **Proper Tool Permissions** + - Use minimal necessary tools + - Document security implications + - Test with restricted tools + +3. **Comprehensive Documentation** + - Provide usage examples + - Document common pitfalls + - Include troubleshooting guide + +### Maintenance + +- Keep version updated +- Test after tool updates +- Monitor user feedback +- Iterate on descriptions + +## Performance Tips + +- Scope skills to specific domains +- Avoid overlapping trigger phrases +- Keep descriptions under 1024 chars +- Test activation reliability + +## Security Considerations + +- Never include secrets in skill files +- Validate all inputs +- Use read-only tools when possible +- Document security requirements diff --git a/skills/skill-adapter/references/examples.md b/skills/skill-adapter/references/examples.md new file mode 100644 index 0000000..b1d8bd2 --- /dev/null +++ b/skills/skill-adapter/references/examples.md @@ -0,0 +1,70 @@ +# Skill Usage Examples + +This document provides practical examples of how to use this skill effectively. + +## Basic Usage + +### Example 1: Simple Activation + +**User Request:** +``` +[Describe trigger phrase here] +``` + +**Skill Response:** +1. Analyzes the request +2. Performs the required action +3. Returns results + +### Example 2: Complex Workflow + +**User Request:** +``` +[Describe complex scenario] +``` + +**Workflow:** +1. Step 1: Initial analysis +2. Step 2: Data processing +3. Step 3: Result generation +4. Step 4: Validation + +## Advanced Patterns + +### Pattern 1: Chaining Operations + +Combine this skill with other tools: +``` +Step 1: Use this skill for [purpose] +Step 2: Chain with [other tool] +Step 3: Finalize with [action] +``` + +### Pattern 2: Error Handling + +If issues occur: +- Check trigger phrase matches +- Verify context is available +- Review allowed-tools permissions + +## Tips & Best Practices + +- ✅ Be specific with trigger phrases +- ✅ Provide necessary context +- ✅ Check tool permissions match needs +- ❌ Avoid vague requests +- ❌ Don't mix unrelated tasks + +## Common Issues + +**Issue:** Skill doesn't activate +**Solution:** Use exact trigger phrases from description + +**Issue:** Unexpected results +**Solution:** Check input format and context + +## See Also + +- Main SKILL.md for full documentation +- scripts/ for automation helpers +- assets/ for configuration examples diff --git a/skills/skill-adapter/scripts/README.md b/skills/skill-adapter/scripts/README.md new file mode 100644 index 0000000..ef69504 --- /dev/null +++ b/skills/skill-adapter/scripts/README.md @@ -0,0 +1,8 @@ +# Scripts + +Bundled resources for api-load-tester skill + +- [ ] k6_load_test.py: Script to generate and execute k6 load tests based on user input. +- [ ] gatling_load_test.py: Script to generate and execute Gatling load tests based on user input. +- [ ] artillery_load_test.py: Script to generate and execute Artillery load tests based on user input. +- [ ] load_test_parser.py: Script to parse load test results and provide a summary. diff --git a/skills/skill-adapter/scripts/helper-template.sh b/skills/skill-adapter/scripts/helper-template.sh new file mode 100755 index 0000000..c4aae90 --- /dev/null +++ b/skills/skill-adapter/scripts/helper-template.sh @@ -0,0 +1,42 @@ +#!/bin/bash +# Helper script template for skill automation +# Customize this for your skill's specific needs + +set -e + +function show_usage() { + echo "Usage: $0 [options]" + echo "" + echo "Options:" + echo " -h, --help Show this help message" + echo " -v, --verbose Enable verbose output" + echo "" +} + +# Parse arguments +VERBOSE=false + +while [[ $# -gt 0 ]]; do + case $1 in + -h|--help) + show_usage + exit 0 + ;; + -v|--verbose) + VERBOSE=true + shift + ;; + *) + echo "Unknown option: $1" + show_usage + exit 1 + ;; + esac +done + +# Your skill logic here +if [ "$VERBOSE" = true ]; then + echo "Running skill automation..." +fi + +echo "✅ Complete" diff --git a/skills/skill-adapter/scripts/validation.sh b/skills/skill-adapter/scripts/validation.sh new file mode 100755 index 0000000..590af58 --- /dev/null +++ b/skills/skill-adapter/scripts/validation.sh @@ -0,0 +1,32 @@ +#!/bin/bash +# Skill validation helper +# Validates skill activation and functionality + +set -e + +echo "🔍 Validating skill..." + +# Check if SKILL.md exists +if [ ! -f "../SKILL.md" ]; then + echo "❌ Error: SKILL.md not found" + exit 1 +fi + +# Validate frontmatter +if ! grep -q "^---$" "../SKILL.md"; then + echo "❌ Error: No frontmatter found" + exit 1 +fi + +# Check required fields +if ! grep -q "^name:" "../SKILL.md"; then + echo "❌ Error: Missing 'name' field" + exit 1 +fi + +if ! grep -q "^description:" "../SKILL.md"; then + echo "❌ Error: Missing 'description' field" + exit 1 +fi + +echo "✅ Skill validation passed"