Claude Skill

api-test-bruno

Use this skill when you need to parse multi-format API definitions and generate Bruno collections for executable regression; triggers include Bruno collections and Bruno API testing.

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download naodeng-awesome-qa-skills-skills_en_testing-types_api-test-bruno-c44b892.zip · 21 KB
Part of naodeng/awesome-qa-skills — 97 skills

Install

skills CLI npx skills add https://github.com/naodeng/awesome-qa-skills/tree/main/skills/en/testing-types/api-test-bruno
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install naodeng-awesome-qa-skills@llmmart
Git git clone https://github.com/naodeng/awesome-qa-skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole naodeng/awesome-qa-skills collection as a plugin from our marketplace. Git is the plain clone.

README

api-test-bruno (EN)

Skill Overview

Need API outputs that should land in Bruno structure or Bruno workflow; The project already uses Bruno or wants Bruno-ready organization.

How to Use

  1. Open SKILL.md in this folder and confirm this skill fits your task.
  2. In your AI tool, call @skill api-test-bruno, then add your real project context and goal.
  3. If you need a specific output format (table, checklist, report), include it directly in your request.

One-Click Install Script

Run from the repository root:

macOS / Linux

bash ./scripts/install-skills-mac.sh --tool codex --lang en --skill api-test-bruno

Windows PowerShell

powershell -ExecutionPolicy Bypass -File .\scripts\install-skills-windows.ps1 -Tool codex -Lang en -Skill api-test-bruno

Skill manifest

api-test-bruno (EN)

Chinese version: See the corresponding Chinese skill.

When to Use

  • Need API outputs that should land in Bruno structure or Bruno workflow.
  • The project already uses Bruno or wants Bruno-ready organization.

Workflow

  1. Read and follow the main prompt listed under Progressive disclosure (coverage, structure, quality bar).
  2. Add only project context that changes the result: scope, environment, constraints, risks, dependencies, expected deliverable.
  3. If input is incomplete, return a usable first draft and explicitly mark assumptions and gaps.
  4. Default to Markdown; switch formats only when the user asks.

Core Constraints

  • Prioritize by risk / business impact — do not treat everything equally.
  • Separate confirmed facts from current assumptions.
  • Do not invent endpoints, fields, environments, or root causes the user did not provide.
  • Use placeholders or env-var semantics for auth/secrets; never hardcode real credentials.
  • Keep output executable: concrete scenarios, clear priority, clear next steps.

Progressive Disclosure

  • Before producing output, read and follow prompts/api-test-bruno.md (minimum coverage, output structure, quality bar).
  • When a ready-made template fits: use matching files under output-templates/.
  • When the user wants examples or alignment with existing assets: read relevant examples/.
  • For deep framework/troubleshoot/schema notes: read only the relevant file(s) under references/, do not load the whole directory.
  • For format conversion or helper checks: prefer existing scripts/ over reinventing.
  • For evaluating/regressing this skill: use evals/ with skill-up.

Pre-delivery Checklist

  • Followed the main prompt's output structure
  • Minimum coverage focus: collection structure, environment setup, auth handling, priority endpoints, positive scenarios, negative and boundary scenarios, data or variable strategy, assertion focus, ... (details in main prompt)
  • Covered the minimum checklist, or explained omissions
  • High-risk items have explicit priority
  • Did not invent details the user did not provide
  • Assumptions and gaps are marked

Common Pitfalls

  • Do not pretend completeness when scope/context is missing.
  • Do not treat every item as equally important.
  • Do not skip assumptions and information gaps.
  • Do not dump generic theory unrelated to the current toolchain.
Files (awesome-qa-skills)
  • agents
    • openai.yaml 471 B
      version: 1
      metadata:
        key: "api-test-bruno"
        last_verified: "2026-03-24"
      interface:
        display_name: "API Test Bruno"
        short_description: "Use this skill when you need to parse multi-format API definitions and generate Bruno collections for executable regression; triggers include Bruno collections…"
        default_prompt: "Use api-test-bruno to complete the task with local scripts, prompts, and examples in this skill folder."
      policy:
        allow_implicit_invocation: true
      
  • evals
    • cases
      • basic-success.yaml 2.5 KB
        id: "basic-success"
        title: "Bruno: OpenAPI/curl yields collection structure"
        description: |
          Use desensitized OpenAPI/curl fixtures to propose a Bruno collection structure.
        
        context:
          files:
            "evals/fixtures/openapi-orders.yaml": |
              openapi: 3.0.3
              info:
                title: Sample Orders API (desensitized fixture)
                version: 0.1.0
              paths:
                /orders:
                  post:
                    summary: Create order
                    security:
                      - bearerAuth: []
                    requestBody:
                      required: true
                      content:
                        application/json:
                          schema:
                            type: object
                            required: [skuId, qty]
                            properties:
                              skuId: { type: string }
                              qty: { type: integer, minimum: 1 }
                    responses:
                      "201":
                        description: Created
                /payments/callback:
                  post:
                    summary: Payment callback
                    responses:
                      "200":
                        description: OK
                /orders/{id}:
                  get:
                    summary: Get order
                    security:
                      - bearerAuth: []
                    parameters:
                      - name: id
                        in: path
                        required: true
                        schema: { type: string }
                    responses:
                      "200":
                        description: OK
              components:
                securitySchemes:
                  bearerAuth:
                    type: http
                    scheme: bearer
            "evals/fixtures/sample-curls.sh": |
              curl -sS -X POST "${BASE_URL}/orders" \
                -H "Authorization: Bearer ${API_TOKEN}" \
                -H "Content-Type: application/json" \
                -d '{"skuId":"SKU-1001","qty":1}'
              curl -sS -X POST "${BASE_URL}/payments/callback" \
                -H "Content-Type: application/json" \
                -d '{"orderId":"ORD-PLACEHOLDER","result":"SUCCESS"}'
              curl -sS "${BASE_URL}/orders/ORD-PLACEHOLDER" \
                -H "Authorization: Bearer ${API_TOKEN}"
        
        input:
          prompt: |
            Use api-test-bruno.
            Fixtures are already in the workspace:
              - evals/fixtures/openapi-orders.yaml
              - evals/fixtures/sample-curls.sh
            Read both files, then propose a Bruno collection structure (.bru / folders / env vars).
            Env: SIT. Auth only via {{token}} / ${API_TOKEN}. No real tokens. No huge code dumps.
        
        expect:
          must_contain:
            - "Bruno"
            - ".bru"
          must_not_contain:
            - "TODO"
            - "I cannot"
        
        judge:
          type: script
          script_path: evals/fixtures/scripts/check_bruno_output.sh
        
      • edge-bad-or-neighbor.yaml 668 B
        id: edge-bad-or-neighbor
        title: "Bruno: redirect Postman/k6 neighbor requests"
        description: |
          When the user asks for Postman/k6 under the Bruno skill, stay on Bruno or clarify the boundary.
        
        input:
          prompt: |
            I activated api-test-bruno, but I asked: generate a Postman collection and a k6 load script.
            Endpoint: GET /health. Correct the scope: deliver a Bruno plan and explain why Postman/k6 are out of this skill's deliverable.
        
        expect:
          must_contain:
            - "Bruno"
            - "Postman"
          must_not_contain:
            - "TODO"
            - "I cannot"
        
        judge:
          type: rule_based
          success:
            - output_contains:
                all:
                  - "Bruno"
                  - "Open Questions"
        
      • edge-incomplete-input.yaml 636 B
        id: edge-incomplete-input
        title: "Bruno: one-line ask still yields draft"
        description: |
          With only one sentence and no schema, still draft Bruno assets and list gaps/assumptions.
        
        input:
          prompt: |
            Use api-test-bruno. I only know: please test the points redemption API. No OpenAPI, no fields, no path.
            Give a usable Bruno collection draft structure and list required gaps and assumptions.
        
        expect:
          must_contain:
            - "assumption"
            - "Open Questions"
          must_not_contain:
            - "TODO"
            - "I cannot"
        
        judge:
          type: rule_based
          success:
            - output_contains:
                all:
                  - "assumption"
                  - "Bruno"
        
    • fixtures
      • scripts
        • check_bruno_output.sh 1.3 KB
          #!/usr/bin/env bash
          # Script judge for Bruno-oriented skill outputs.
          # skill-up provides: EVAL_FINAL_MESSAGE, EVAL_EXIT_CODE, EVAL_TRANSCRIPT_PATH
          set -euo pipefail
          
          text="${EVAL_FINAL_MESSAGE:-}"
          if [[ -z "$text" && -n "${EVAL_TRANSCRIPT_PATH:-}" && -f "$EVAL_TRANSCRIPT_PATH" ]]; then
            text="$(cat "$EVAL_TRANSCRIPT_PATH")"
          fi
          if [[ -z "$text" && -f "outputs/response.md" ]]; then
            text="$(cat outputs/response.md)"
          fi
          if [[ -z "$text" ]]; then
            # last resort: search workspace
            f="$(find . -name response.md 2>/dev/null | head -1 || true)"
            if [[ -n "$f" ]]; then
              text="$(cat "$f")"
            fi
          fi
          
          if [[ -z "$text" ]]; then
            echo "FAIL: empty final message / no response.md"
            exit 1
          fi
          
          fail=0
          need_any() {
            local label="$1"
            shift
            local ok=0
            local k
            for k in "$@"; do
              if grep -Fqi -- "$k" <<<"$text"; then
                ok=1
                break
              fi
            done
            if [[ "$ok" -eq 0 ]]; then
              echo "FAIL missing any of: $*"
              fail=1
            else
              echo "OK: $label"
            fi
          }
          
          need_any "Bruno marker" "Bruno" "bruno"
          need_any "collection marker" "collection" "Collection" "集合" ".bru"
          
          if grep -Eiq 'Bearer [A-Za-z0-9_\-]{20,}' <<<"$text"; then
            echo "FAIL: looks like a hardcoded bearer token"
            fail=1
          else
            echo "OK: no long bearer literal"
          fi
          
          if [[ "$fail" -ne 0 ]]; then
            exit 1
          fi
          exit 0
          
      • openapi-orders.yaml 1.8 KB
        openapi: 3.0.3
        info:
          title: Sample Orders API (desensitized fixture)
          version: 0.1.0
        paths:
          /orders:
            post:
              summary: Create order
              operationId: createOrder
              security:
                - bearerAuth: []
              requestBody:
                required: true
                content:
                  application/json:
                    schema:
                      type: object
                      required: [skuId, qty]
                      properties:
                        skuId:
                          type: string
                        qty:
                          type: integer
                          minimum: 1
              responses:
                "201":
                  description: Created
                  content:
                    application/json:
                      schema:
                        type: object
                        properties:
                          orderId:
                            type: string
                          status:
                            type: string
                "401":
                  description: Unauthorized
          /payments/callback:
            post:
              summary: Payment callback
              operationId: paymentCallback
              requestBody:
                required: true
                content:
                  application/json:
                    schema:
                      type: object
                      required: [orderId, result]
                      properties:
                        orderId:
                          type: string
                        result:
                          type: string
                          enum: [SUCCESS, FAIL]
              responses:
                "200":
                  description: OK
          /orders/{id}:
            get:
              summary: Get order
              operationId: getOrder
              security:
                - bearerAuth: []
              parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                    type: string
              responses:
                "200":
                  description: OK
        components:
          securitySchemes:
            bearerAuth:
              type: http
              scheme: bearer
        
      • sample-curls.sh 551 B
        # Desensitized curl examples for API automation fixtures.
        # Use placeholders only — never real tokens.
        
        # Create order
        curl -sS -X POST "${BASE_URL}/orders" \
          -H "Authorization: Bearer ${API_TOKEN}" \
          -H "Content-Type: application/json" \
          -d '{"skuId":"SKU-1001","qty":1}'
        
        # Payment callback
        curl -sS -X POST "${BASE_URL}/payments/callback" \
          -H "Content-Type: application/json" \
          -d '{"orderId":"ORD-PLACEHOLDER","result":"SUCCESS"}'
        
        # Get order
        curl -sS "${BASE_URL}/orders/ORD-PLACEHOLDER" \
          -H "Authorization: Bearer ${API_TOKEN}"
        
    • eval.yaml 587 B
      schema_version: v1alpha1
      
      environment:
        type: none
      
      skills:
        - source: local_path
          path: .
      
      engine:
        name: claude_code
        # model is optional; omit to use engine default
        # model:
        #   provider: anthropic
        #   name: claude-sonnet-4-6
      
      cases:
        files:
          - evals/cases/basic-success.yaml
          - evals/cases/edge-incomplete-input.yaml
          - evals/cases/edge-bad-or-neighbor.yaml
        defaults:
          timeout_seconds: 180
          max_turns: 8
          expect:
            exit_code: 0
            must_not_contain:
              - "TODO"
              - "I cannot"
      
      benchmark:
        enabled: true
      
      report:
        formats: [json]
      
    • trigger_queries.json 927 B
      [
        {"query": "Generate a Bruno collection from this OpenAPI for SIT with token vars", "should_trigger": true},
        {"query": "Organize these curls into a Bruno collection with assertions", "should_trigger": true},
        {"query": "How should I structure Bruno folders for orders and payment callback?", "should_trigger": true},
        {"query": "Use Bruno for smoke coverage on auth-related APIs", "should_trigger": true},
        {"query": "Parse swagger into an executable API collection — we use Bruno", "should_trigger": true},
        {"query": "Load test /orders with k6, P95 under 300ms", "should_trigger": false},
        {"query": "Write Pytest API automation, not Bruno", "should_trigger": false},
        {"query": "Write functional test cases for checkout and shipping", "should_trigger": false},
        {"query": "File a bug report for double charging", "should_trigger": false},
        {"query": "Playwright E2E for login SSO", "should_trigger": false}
      ]
      
  • examples
    • bruno
      • sample.bru 137 B · in bundle
    • ci
      • github-actions-bruno.yml 853 B
        name: API Bruno CI
        
        on:
          workflow_dispatch:
          pull_request:
            paths:
              - "explore/api-test-bruno/**"
        
        jobs:
          test:
            runs-on: ubuntu-latest
            defaults:
              run:
                working-directory: explore/api-test-bruno/scripts
            steps:
              - name: Checkout
                uses: actions/checkout@v4
        
              - name: Setup Node
                uses: actions/setup-node@v4
                with:
                  node-version: "20"
        
              - name: Install Bruno CLI
                run: npm i -g @usebruno/cli
        
              - name: Run Bruno tests
                env:
                  BASE_URL: ${{ secrets.API_BASE_URL }}
                  API_TOKEN: ${{ secrets.API_TOKEN }}
                run: ./run-tests.sh ./templates/bruno staging
        
              - name: Upload reports
                if: always()
                uses: actions/upload-artifact@v4
                with:
                  name: bruno-reports
                  path: explore/api-test-bruno/reports/**
        
      • Jenkinsfile.bruno 599 B · in bundle
    • sample.curl 212 B · in bundle
    • sample.insomnia.json 502 B
      {
        "_type": "export",
        "__export_format": 4,
        "__export_date": "2026-03-23T00:00:00.000Z",
        "__export_source": "insomnia.desktop.app:v9.0.0",
        "resources": [
          {
            "_id": "req_1",
            "_type": "request",
            "name": "List Products",
            "method": "GET",
            "url": "https://api.example.com/v1/products"
          },
          {
            "_id": "req_2",
            "_type": "request",
            "name": "Create Product",
            "method": "POST",
            "url": "https://api.example.com/v1/products"
          }
        ]
      }
      
    • sample.openapi.yaml 240 B
      openapi: 3.0.3
      info:
        title: Sample API
        version: 1.0.0
      paths:
        /v1/users:
          get:
            summary: list users
          post:
            summary: create user
        /v1/users/{id}:
          get:
            summary: get user
          delete:
            summary: delete user
      
    • sample.opencollection.json 243 B
      {
        "name": "OpenCollection Sample",
        "requests": [
          {
            "name": "Get Orders",
            "method": "GET",
            "path": "/v1/orders"
          },
          {
            "name": "Create Order",
            "method": "POST",
            "path": "/v1/orders"
          }
        ]
      }
      
    • sample.postman_collection.json 704 B
      {
        "info": {
          "name": "Sample API Collection",
          "_postman_id": "b93a09ab-2222-4444-8888-444455556666",
          "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
        },
        "item": [
          {
            "name": "Get Users",
            "request": {
              "method": "GET",
              "url": {
                "raw": "https://api.example.com/v1/users",
                "path": [
                  "v1",
                  "users"
                ]
              }
            }
          },
          {
            "name": "Create User",
            "request": {
              "method": "POST",
              "url": {
                "raw": "https://api.example.com/v1/users",
                "path": [
                  "v1",
                  "users"
                ]
              }
            }
          }
        ]
      }
      
    • sample.wsdl 534 B · in bundle
  • output-templates
    • template-markdown.md 83 B
      # Output Template
      
      ## Summary
      - Scope:
      - Key Points:
      
      ## Details
      - Item 1
      - Item 2
      
  • prompts
    • api-test-bruno.md 5.7 KB
      # API Test Bruno Prompt
      
      From the materials the user provides, produce a Bruno collection plan or test-asset structure the team can implement directly.
      
      ## Role
      
      - Act as a senior QA and API automation expert who turns API materials into a maintainable Bruno collection.
      
      
      ## Input parsing order
      
      Parse in this priority order. Higher priority wins on conflicts; when sources disagree, state the conflict and source — **do not silently invent a merged “truth”**:
      
      1. Existing Bruno assets (`.bru` / `bruno.json` / collection tree)
      2. OpenAPI / Swagger (`openapi.yaml` / `swagger.json`)
      3. Postman Collection, Insomnia, or OpenCollection
      4. curl examples (headers / query / body)
      5. Loose notes (tables, Markdown, verbal endpoint lists)
      
      Also absorb when present: business scope, auth model, environment baseUrl, release priority, CI needs, existing folder conventions.
      
      Extract only paths, methods, params, fields, and sample values that **actually appear** in the materials. Put gaps in “missing information”; do not complete a fake full API doc.
      
      ## Defaults (use these unless the user specifies otherwise)
      
      Prefer defaults; do not present a tool menu.
      
      **Directory layout**
      
      ```text
      <collection-name>/
        bruno.json
        environments/
          local.bru
          staging.bru
        <folder-by-resource-or-flow>/
          <request-name>.bru
      ```
      
      **Naming**
      
      - collection `name`: short domain name (e.g. `order-api`)
      - request `meta.name`: `kebab-case` method+resource (e.g. `get-users`, `create-order`)
      - folders: by resource or critical flow — not one flat dump of every request
      
      **Environments and variables**
      
      - Standard vars: `{{baseUrl}}`, `{{token}}` (reuse project names if they already exist)
      - Secrets only as placeholders in `environments/*.bru` (e.g. `replace-me`) or “inject from CI secret / local env”
      - Request URLs: `{{baseUrl}}/path` — do not hardcode host into every `.bru`
      
      **Assertion style**
      
      - Each high-priority request: at least `status` + one critical response field (when the doc has fields)
      - Bruno `tests` blocks with `expect(res.getStatus()).to.equal(...)`; assert JSON fields when present
      - Separate layers via folders or name prefixes: `smoke` / `contract` / `business` / `negative`
      
      **Run defaults**
      
      - Local: Bruno CLI / GUI against a folder or tag
      - CI: smoke folder first, then expand regression; secrets via CI secrets, never committed
      
      If a collection already exists, **align to it** and apply these defaults only where gaps remain.
      
      ## Gotchas
      
      - **Never** hardcode real Bearer tokens, passwords, cookies, or private keys in examples, env files, or output; use placeholders or “read from env” notes.
      - When migrating from curl/Postman: **redact** Authorization / Cookie / signing headers before writing them into the plan.
      - **Do not invent** paths, query/header/body fields, status codes, or error codes the user did not provide; mark assumptions or gaps.
      - Do not rewrite the Bruno plan as Postman/Newman, pytest, k6, or other unrelated stacks.
      - If information is incomplete, still ship a usable first version (structure + confirmed endpoints) and list assumptions explicitly.
      - Unless the user asks for runnable `.bru` contents, prefer structure notes + key request points over huge full-file dumps.
      
      ## Minimum coverage checklist
      
      Unless the user explicitly narrows scope, the result must cover:
      
      - collection directory and folder split
      - environment variables (`baseUrl` / auth placeholders)
      - how auth and permission-related requests are handled
      - high-priority endpoints (with P0/P1)
      - positive scenarios
      - negative and boundary scenarios (at least documented validation/error paths)
      - variables and test-data strategy (create/cleanup needs)
      - assertion focus (status + critical fields)
      - smoke vs regression scope
      - CI or local run guidance
      - missing information and assumptions
      
      ## Output
      
      Return results in this order (keep sections; make each concrete):
      
      ### 1. Task Understanding
      - API / domain under test
      - goal (new collection / strengthen / migrate from another format)
      - in-scope endpoints or flows
      - out-of-scope or unclear areas
      - input sources (OpenAPI / Postman / curl / …) and how conflicts were handled
      
      ### 2. Bruno Collection Plan
      - proposed collection tree (concrete folder names)
      - `environments` variable list (name, purpose, placeholder example; no real secrets)
      - auth default: request-level / shared script / env vars — which layer
      - alignment with existing assets (if any)
      
      ### 3. Priority Request Coverage
      For each P0/P1 request:
      - `meta.name` / method / path (confirmed only)
      - folder
      - priority and risk rationale
      - positive checks
      - negative / boundary checks
      - assertion focus (status, fields)
      - prerequisite requests or variables
      
      ### 4. Execution Notes
      - suggested order (auth → writes → read checks → cleanup)
      - smoke folder / request list
      - regression expansion
      - release-blocking checks
      
      ### 5. Automation and CI Suggestions
      - how to run locally
      - minimal CI steps (Bruno CLI, select env, run smoke)
      - secret injection contract (variable names only)
      
      ### 6. Open Questions
      - information gaps
      - assumptions used this round (itemized)
      
      ## Pre-delivery checklist
      
      - [ ] Inputs followed the parsing order; conflicts and gaps are called out
      - [ ] Layout / naming / `{{baseUrl}}`+`{{token}}` placeholders match defaults (or explain reuse of existing)
      - [ ] No real secrets; no invented paths/fields/status codes
      - [ ] P0/P1 requests have concrete scenarios and assertions — not vague “cover happy and unhappy paths”
      - [ ] All six output sections present; smoke and CI are actionable
      
      ## Quality bar
      
      - Stay Bruno-specific: folders, request names, variable names.
      - Prioritize by risk; do not treat every endpoint equally.
      - Separate confirmed facts from assumptions.
      - Avoid huge full `.bru` dumps unless the user asks for runnable files.
      
  • references
    • local
      • api-testing_EN-1.md 487 B
        # Archived Local Reference
        
        This file is a lightweight legacy note. The previous long snapshot was removed to avoid duplicate and outdated guidance.
        
        ## Use Instead
        
        - Main prompt: `prompts/api-test-bruno.md`
        - Main entry: `SKILL.md`
        
        ## Notes
        
        - Keep using the current prompt and `SKILL.md` as the source of truth.
        - Load `references/`, `examples/`, or `scripts/` only when the task really needs extra detail.
        - Do not rely on this file for the latest prompt wording or workflow rules.
        
      • api-testing_EN.md 487 B
        # Archived Local Reference
        
        This file is a lightweight legacy note. The previous long snapshot was removed to avoid duplicate and outdated guidance.
        
        ## Use Instead
        
        - Main prompt: `prompts/api-test-bruno.md`
        - Main entry: `SKILL.md`
        
        ## Notes
        
        - Keep using the current prompt and `SKILL.md` as the source of truth.
        - Load `references/`, `examples/`, or `scripts/` only when the task really needs extra detail.
        - Do not rely on this file for the latest prompt wording or workflow rules.
        
    • framework-spec.md 1.5 KB
      # Bruno Framework Specification
      
      ## 1. Directory Convention
      
      - `scripts/parse_api_sources.py`: multi-format API source parser
      - `scripts/generate_bruno_requests.py`: normalized endpoint -> Bruno requests
      - `scripts/templates/bruno/`: runnable Bruno baseline collection
      - `generated-bruno/`: generated collection output
      
      ## 2. Test Layer Convention
      
      - `smoke`: core endpoint availability
      - `contract`: status code + response shape checks
      - `business`: key API flow and state transition checks
      - `negative`: invalid params, auth failure, boundary inputs
      
      ## 3. Assertion Rules
      
      - status code checks are mandatory
      - response-time checks for critical endpoints are recommended
      - negative tests must validate error structure
      - use environment-scoped variables for auth and host
      
      ## 4. Data and Environment Rules
      
      - use `{{baseUrl}}` and token variables
      - never commit secrets into collection files
      - keep test data deterministic or seed-controlled
      - define cleanup strategy for mutable endpoints
      
      ## 5. Parsing Scope Rules
      
      Supported parser inputs:
      - curl
      - Postman collections
      - Swagger/OpenAPI (including v3)
      - Bruno
      - OpenCollection
      - Insomnia
      - WSDL
      - ZIP containing any supported format
      
      Parser output must be normalized endpoint JSON with:
      - method
      - path
      - source format
      - optional base URL, headers, query/body hints
      
      ## 6. CI Guidance
      
      - run smoke collection on each PR
      - run full collection on main/nightly
      - fail on P0 regressions
      - export machine-readable reports for trend tracking
      
    • report-schema.md 800 B
      # Unified API Test Report Schema
      
      Use these fields for all generated API test reports:
      
      - `run_id`: unique run identifier
      - `tool`: `supertest` / `pytest` / `bruno` / `restassured`
      - `env`: environment name (staging/prod-like)
      - `case_id`: stable case identifier
      - `api_name`: human-readable endpoint name
      - `method`: HTTP method
      - `path`: endpoint path
      - `status`: `pass` / `fail` / `skip`
      - `status_code`: actual HTTP status code
      - `expected_status_code`: expected status code or range
      - `latency_ms`: measured request latency
      - `error_rate`: optional aggregated failure rate per run
      - `assertions_total`: total assertions
      - `assertions_passed`: passed assertions
      - `message`: failure or debug message
      - `timestamp`: ISO8601 UTC timestamp
      
      Recommended rule:
      - P0 failures should fail CI pipelines.
      
    • setup-and-ci.md 447 B
      # Setup and CI Guide
      
      ## Local
      
      ```bash
      npm i -g @usebruno/cli
      cd scripts
      ./run-tests.sh ./templates/bruno staging
      ```
      
      ## One-Click Flow
      
      ```bash
      cd scripts
      ./run.sh ../examples
      ```
      
      ## CI Recommendation
      
      - PR: run smoke subset
      - main/nightly: run full generated collection
      - archive json report output
      
      ## CI Template
      
      - `examples/ci/github-actions-bruno.yml`
      - `examples/ci/Jenkinsfile.bruno`
      
      ## Report Schema
      
      - `references/report-schema.md`
      
  • scripts
    • templates
      • bruno
        • environments
          • staging.bru 64 B · in bundle
        • requests
          • health.bru 251 B · in bundle
        • bruno.json 74 B
          {
            "version": "1",
            "name": "super-api-tests",
            "type": "collection"
          }
          
    • generate_bruno_requests.py 2.6 KB
      #!/usr/bin/env python3
      import argparse
      import json
      import re
      from pathlib import Path
      
      
      def _safe_slug(method: str, path: str, i: int) -> str:
          base = f"{i:03d}_{method}_{path}".lower()
          return re.sub(r"[^a-z0-9]+", "_", base).strip("_")
      
      
      def _to_bru(method: str, path: str, name: str) -> str:
          method_lower = method.lower()
          body_mode = "none" if method_lower in {"get", "delete", "head", "options"} else "json"
          body_block = ""
          if body_mode == "json":
              body_block = 'body:json {\n  {\n    "sample": true\n  }\n}\n'
          return (
              "meta {\n"
              f"  name: {name}\n"
              "  type: http\n"
              "  seq: 1\n"
              "}\n\n"
              f"{method_lower} {{\n"
              f"  url: {{baseUrl}}{path}\n"
              f"  body: {body_mode}\n"
              "}\n\n"
              f"{body_block}\n"
              "headers {\n"
              "  Content-Type: application/json\n"
              "  Authorization: Bearer {{token}}\n"
              "}\n\n"
              "tests {\n"
              "  test(\"status should be expected\", function() {\n"
              "    const status = res.getStatus();\n"
              "    expect([200, 201, 202, 204, 400, 401, 403, 404]).to.include(status);\n"
              "  });\n"
              "}\n"
          )
      
      
      def _bruno_json() -> str:
          return '{\n  "version": "1",\n  "name": "generated-bruno-collection",\n  "type": "collection"\n}\n'
      
      
      def _env_bru() -> str:
          return (
              "vars {\n"
              "  baseUrl: https://api.example.com\n"
              "  token: replace-me\n"
              "}\n"
          )
      
      
      def main() -> None:
          parser = argparse.ArgumentParser(description="Generate Bruno requests from normalized endpoint JSON")
          parser.add_argument("--input", required=True, type=Path, help="Normalized endpoint JSON")
          parser.add_argument("--output-dir", required=True, type=Path, help="Output Bruno collection folder")
          args = parser.parse_args()
      
          payload = json.loads(args.input.read_text(encoding="utf-8"))
          endpoints = payload.get("endpoints", [])
      
          out_dir = args.output_dir
          req_dir = out_dir / "requests"
          env_dir = out_dir / "environments"
          req_dir.mkdir(parents=True, exist_ok=True)
          env_dir.mkdir(parents=True, exist_ok=True)
      
          (out_dir / "bruno.json").write_text(_bruno_json(), encoding="utf-8")
          (env_dir / "staging.bru").write_text(_env_bru(), encoding="utf-8")
      
          for i, ep in enumerate(endpoints, start=1):
              method = str(ep.get("method", "GET")).upper()
              path = str(ep.get("path", "/"))
              name = _safe_slug(method, path, i)
              content = _to_bru(method, path, name)
              (req_dir / f"{name}.bru").write_text(content, encoding="utf-8")
      
          print(str(out_dir))
      
      
      if __name__ == "__main__":
          main()
      
    • parse_api_sources.py 9.1 KB
      #!/usr/bin/env python3
      import argparse
      import json
      import re
      import zipfile
      from pathlib import Path
      from typing import Dict, List, Optional
      from xml.etree import ElementTree as ET
      
      try:
          import yaml  # type: ignore
      except Exception:
          yaml = None
      
      
      METHODS = {"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"}
      
      
      def _safe_text(raw: bytes) -> str:
          return raw.decode("utf-8", errors="ignore")
      
      
      def _append_endpoint(out: List[Dict], method: str, path: str, source: str, **extra) -> None:
          method = method.upper().strip()
          path = (path or "").strip()
          if method not in METHODS or not path:
              return
          rec = {"method": method, "path": path, "source": source}
          rec.update({k: v for k, v in extra.items() if v is not None})
          out.append(rec)
      
      
      def _parse_curl(text: str, source: str) -> List[Dict]:
          out: List[Dict] = []
          for line in [x.strip() for x in text.splitlines() if "curl " in x]:
              m_method = re.search(r"-X\s+([A-Za-z]+)", line)
              method = m_method.group(1).upper() if m_method else "GET"
              m_url = re.search(r"(https?://[^\s'\"\\]+)", line)
              if not m_url:
                  continue
              url = m_url.group(1)
              m_path = re.match(r"https?://[^/]+(.*)", url)
              path = m_path.group(1) if m_path else "/"
              if not path:
                  path = "/"
              _append_endpoint(out, method, path, source, raw_url=url, format="curl")
          return out
      
      
      def _parse_postman(obj: Dict, source: str) -> List[Dict]:
          out: List[Dict] = []
      
          def walk(items: List[Dict]) -> None:
              for it in items or []:
                  if "request" in it:
                      req = it.get("request", {})
                      method = str(req.get("method", "GET")).upper()
                      url_obj = req.get("url", {})
                      path = "/"
                      raw_url = None
                      if isinstance(url_obj, dict):
                          raw_url = url_obj.get("raw")
                          path_parts = url_obj.get("path")
                          if isinstance(path_parts, list) and path_parts:
                              path = "/" + "/".join(str(p) for p in path_parts)
                          elif isinstance(raw_url, str):
                              m = re.match(r"https?://[^/]+(.*)", raw_url)
                              path = m.group(1) if m and m.group(1) else "/"
                      elif isinstance(url_obj, str):
                          raw_url = url_obj
                          m = re.match(r"https?://[^/]+(.*)", url_obj)
                          path = m.group(1) if m and m.group(1) else "/"
                      _append_endpoint(out, method, path, source, raw_url=raw_url, format="postman")
                  if isinstance(it.get("item"), list):
                      walk(it["item"])
      
          walk(obj.get("item", []))
          return out
      
      
      def _parse_openapi(obj: Dict, source: str) -> List[Dict]:
          out: List[Dict] = []
          paths = obj.get("paths", {})
          if not isinstance(paths, dict):
              return out
          for path, methods in paths.items():
              if not isinstance(methods, dict):
                  continue
              for m in methods.keys():
                  method = str(m).upper()
                  if method in METHODS:
                      _append_endpoint(out, method, str(path), source, format="openapi")
          return out
      
      
      def _parse_insomnia(obj: Dict, source: str) -> List[Dict]:
          out: List[Dict] = []
          resources = obj.get("resources", [])
          if not isinstance(resources, list):
              return out
          for r in resources:
              if not isinstance(r, dict):
                  continue
              if r.get("_type") == "request":
                  method = str(r.get("method", "GET")).upper()
                  url = str(r.get("url", ""))
                  path = "/"
                  m = re.match(r"https?://[^/]+(.*)", url)
                  if m and m.group(1):
                      path = m.group(1)
                  elif url.startswith("/"):
                      path = url
                  _append_endpoint(out, method, path, source, raw_url=url, format="insomnia")
          return out
      
      
      def _parse_opencollection(obj: Dict, source: str) -> List[Dict]:
          out: List[Dict] = []
          requests = obj.get("requests")
          if isinstance(requests, list):
              for r in requests:
                  if not isinstance(r, dict):
                      continue
                  method = str(r.get("method", "GET")).upper()
                  path = str(r.get("path") or r.get("url") or "/")
                  _append_endpoint(out, method, path, source, format="opencollection")
          return out
      
      
      def _parse_wsdl(text: str, source: str) -> List[Dict]:
          out: List[Dict] = []
          try:
              root = ET.fromstring(text.encode("utf-8"))
          except Exception:
              return out
          ns = {
              "wsdl": "http://schemas.xmlsoap.org/wsdl/",
              "soap": "http://schemas.xmlsoap.org/wsdl/soap/",
          }
          soap_address = root.find(".//soap:address", ns)
          location = soap_address.attrib.get("location") if soap_address is not None else None
          for op in root.findall(".//wsdl:operation", ns):
              name = op.attrib.get("name")
              if not name:
                  continue
              path = f"/soap/{name}"
              _append_endpoint(out, "POST", path, source, raw_url=location, format="wsdl", operation=name)
          return out
      
      
      def _parse_bruno(text: str, source: str) -> List[Dict]:
          out: List[Dict] = []
          method = None
          url = None
          for line in text.splitlines():
              s = line.strip()
              block = re.match(r"^(get|post|put|patch|delete|head|options)\s*\{", s, re.IGNORECASE)
              if block:
                  method = block.group(1).upper()
                  continue
              m = re.match(r"method:\s*([A-Za-z]+)", s, re.IGNORECASE)
              if m:
                  method = m.group(1).upper()
                  continue
              u = re.match(r"url:\s*(.+)$", s, re.IGNORECASE)
              if u:
                  url = u.group(1).strip()
          if method and url:
              m = re.match(r"https?://[^/]+(.*)", url)
              path = m.group(1) if m and m.group(1) else (url if url.startswith("/") else f"/{url}")
              _append_endpoint(out, method, path, source, raw_url=url, format="bruno")
          return out
      
      
      def _load_structured(path: Path, raw: str) -> Optional[Dict]:
          if path.suffix.lower() == ".json":
              try:
                  return json.loads(raw)
              except Exception:
                  return None
          if path.suffix.lower() in {".yaml", ".yml"} and yaml is not None:
              try:
                  obj = yaml.safe_load(raw)
                  return obj if isinstance(obj, dict) else None
              except Exception:
                  return None
          return None
      
      
      def parse_file(path: Path, raw: str) -> List[Dict]:
          lower_name = path.name.lower()
          ext = path.suffix.lower()
      
          if ext == ".wsdl" or (ext == ".xml" and "<definitions" in raw and "wsdl" in raw.lower()):
              return _parse_wsdl(raw, str(path))
          if ext == ".bru":
              return _parse_bruno(raw, str(path))
          if ext in {".curl", ".sh", ".txt"}:
              return _parse_curl(raw, str(path))
      
          obj = _load_structured(path, raw)
          if obj is not None:
              if "openapi" in obj or "swagger" in obj:
                  return _parse_openapi(obj, str(path))
              if lower_name.endswith(".postman_collection.json") or "info" in obj and "item" in obj:
                  return _parse_postman(obj, str(path))
              if "resources" in obj:
                  return _parse_insomnia(obj, str(path))
              if lower_name.endswith(".opencollection.json") or "requests" in obj:
                  return _parse_opencollection(obj, str(path))
              return []
      
          return _parse_curl(raw, str(path))
      
      
      def parse_zip(path: Path) -> List[Dict]:
          out: List[Dict] = []
          with zipfile.ZipFile(path) as zf:
              for info in zf.infolist():
                  if info.is_dir():
                      continue
                  p = Path(info.filename)
                  raw = _safe_text(zf.read(info))
                  if p.suffix.lower() == ".zip":
                      continue
                  out.extend(parse_file(p, raw))
          return out
      
      
      def parse_input(path: Path) -> List[Dict]:
          if path.is_dir():
              out: List[Dict] = []
              for f in path.rglob("*"):
                  if f.is_file():
                      raw = f.read_text(encoding="utf-8", errors="ignore")
                      out.extend(parse_file(f, raw))
              return out
          if path.suffix.lower() == ".zip":
              return parse_zip(path)
          raw = path.read_text(encoding="utf-8", errors="ignore")
          return parse_file(path, raw)
      
      
      def dedupe(endpoints: List[Dict]) -> List[Dict]:
          seen = set()
          out = []
          for e in endpoints:
              key = (e.get("method"), e.get("path"), e.get("source"))
              if key in seen:
                  continue
              seen.add(key)
              out.append(e)
          return out
      
      
      def main() -> None:
          parser = argparse.ArgumentParser(description="Parse API sources into normalized endpoint inventory")
          parser.add_argument("--input", required=True, type=Path, help="Input file/folder/zip path")
          parser.add_argument("--output", type=Path, help="Output JSON path")
          args = parser.parse_args()
      
          endpoints = dedupe(parse_input(args.input))
          payload = {
              "input": str(args.input),
              "count": len(endpoints),
              "endpoints": endpoints,
          }
      
          if args.output:
              args.output.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
              print(str(args.output))
          else:
              print(json.dumps(payload, ensure_ascii=False, indent=2))
      
      
      if __name__ == "__main__":
          main()
      
    • run-tests.sh 713 B
      #!/usr/bin/env bash
      set -euo pipefail
      
      SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
      COLLECTION_DIR="${1:-$SCRIPT_DIR/templates/bruno}"
      ENV_NAME="${2:-staging}"
      REPORT_DIR="$SCRIPT_DIR/../reports"
      mkdir -p "$REPORT_DIR"
      
      if ! command -v bru >/dev/null 2>&1; then
        echo "bru command not found. Please install Bruno CLI first."
        echo "npm i -g @usebruno/cli"
        exit 1
      fi
      
      TS="$(date +%Y%m%d-%H%M%S)"
      REPORT_JSON="$REPORT_DIR/bru-report-${TS}.json"
      
      echo "Running Bruno collection: $COLLECTION_DIR (env=$ENV_NAME)"
      
      # Note: CLI flags may vary by Bruno version.
      # This command targets common CLI usage.
      bru run "$COLLECTION_DIR" --env "$ENV_NAME" --reporter-json "$REPORT_JSON"
      
      echo "Report generated: $REPORT_JSON"
      
    • run.sh 582 B
      #!/usr/bin/env bash
      set -euo pipefail
      
      SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
      ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
      INPUT_PATH="${1:-$ROOT_DIR/examples}"
      TMP_JSON="$ROOT_DIR/.tmp.normalized.json"
      OUT_DIR="$ROOT_DIR/generated-bruno"
      
      python3 "$SCRIPT_DIR/parse_api_sources.py" --input "$INPUT_PATH" --output "$TMP_JSON"
      python3 "$SCRIPT_DIR/generate_bruno_requests.py" --input "$TMP_JSON" --output-dir "$OUT_DIR"
      
      if ! command -v bru >/dev/null 2>&1; then
        echo "bru not found. Collection generated at: $OUT_DIR"
        exit 0
      fi
      
      "$SCRIPT_DIR/run-tests.sh" "$OUT_DIR" "staging"
      
  • README.md 808 B
    # api-test-bruno (EN)
    
    ## Skill Overview
    
    Need API outputs that should land in Bruno structure or Bruno workflow; The project already uses Bruno or wants Bruno-ready organization.
    
    ## How to Use
    
    1. Open `SKILL.md` in this folder and confirm this skill fits your task.
    2. In your AI tool, call `@skill api-test-bruno`, then add your real project context and goal.
    3. If you need a specific output format (table, checklist, report), include it directly in your request.
    
    ## One-Click Install Script
    
    Run from the repository root:
    
    ### macOS / Linux
    
    ```bash
    bash ./scripts/install-skills-mac.sh --tool codex --lang en --skill api-test-bruno
    ```
    
    ### Windows PowerShell
    
    ```powershell
    powershell -ExecutionPolicy Bypass -File .\scripts\install-skills-windows.ps1 -Tool codex -Lang en -Skill api-test-bruno
    ```
    
  • SKILL.md 2.6 KB
    ---
    name: api-test-bruno
    description: Use this skill when you need to parse multi-format API definitions and generate Bruno collections for executable regression; triggers include Bruno collections and Bruno API testing.
    ---
    
    # api-test-bruno (EN)
    
    **Chinese version:** See the corresponding Chinese skill.
    
    ## When to Use
    
    - Need API outputs that should land in Bruno structure or Bruno workflow.
    - The project already uses Bruno or wants Bruno-ready organization.
    
    ## Workflow
    
    1. Read and follow the main prompt listed under Progressive disclosure (coverage, structure, quality bar).
    2. Add only project context that changes the result: scope, environment, constraints, risks, dependencies, expected deliverable.
    3. If input is incomplete, return a usable first draft and explicitly mark assumptions and gaps.
    4. Default to Markdown; switch formats only when the user asks.
    
    ## Core Constraints
    
    - Prioritize by risk / business impact — do not treat everything equally.
    - Separate confirmed facts from current assumptions.
    - Do not invent endpoints, fields, environments, or root causes the user did not provide.
    - Use placeholders or env-var semantics for auth/secrets; never hardcode real credentials.
    - Keep output executable: concrete scenarios, clear priority, clear next steps.
    
    ## Progressive Disclosure
    
    - Before producing output, read and follow `prompts/api-test-bruno.md` (minimum coverage, output structure, quality bar).
    - When a ready-made template fits: use matching files under `output-templates/`.
    - When the user wants examples or alignment with existing assets: read relevant `examples/`.
    - For deep framework/troubleshoot/schema notes: read only the relevant file(s) under `references/`, do not load the whole directory.
    - For format conversion or helper checks: prefer existing `scripts/` over reinventing.
    - For evaluating/regressing this skill: use `evals/` with skill-up.
    
    ## Pre-delivery Checklist
    
    - [ ] Followed the main prompt's output structure
    - [ ] Minimum coverage focus: collection structure, environment setup, auth handling, priority endpoints, positive scenarios, negative and boundary scenarios, data or variable strategy, assertion focus, ... (details in main prompt)
    - [ ] Covered the minimum checklist, or explained omissions
    - [ ] High-risk items have explicit priority
    - [ ] Did not invent details the user did not provide
    - [ ] Assumptions and gaps are marked
    
    ## Common Pitfalls
    
    - Do not pretend completeness when scope/context is missing.
    - Do not treat every item as equally important.
    - Do not skip assumptions and information gaps.
    - Do not dump generic theory unrelated to the current toolchain.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related