Skip to content

Latest commit

 

History

History
295 lines (213 loc) · 7.02 KB

File metadata and controls

295 lines (213 loc) · 7.02 KB

HyperFleet E2E - AI Agent Instructions

This file provides context for AI agents working with the HyperFleet E2E testing framework.

Project Overview

Black-box E2E testing framework for HyperFleet cluster lifecycle management. Built with Go, Ginkgo, and OpenAPI-generated clients. Tests create ephemeral clusters for complete isolation.

Build and Test Commands

Build Binary

make build

Binary output: bin/hyperfleet-e2e

Run All Checks

make check

This runs: format check, vet, lint, and unit tests.

Individual Quality Checks

make fmt           # Format code
make fmt-check     # Verify formatting
make vet           # Run go vet
make lint          # Run golangci-lint
make test          # Run unit tests

Generate API Client

Required after OpenAPI schema updates:

make generate

Downloads schema from hyperfleet-api and regenerates pkg/api/openapi/.

Run E2E Tests

export HYPERFLEET_API_URL=https://api.hyperfleet.example.com
make build
./bin/hyperfleet-e2e test --label-filter=tier0

Clean Build Artifacts

make clean

Validation Checklist

Before submitting changes, verify:

  1. Format: make fmt
  2. Generate: make generate (if OpenAPI schema or config changed)
  3. Lint: make lint (must pass with zero errors)
  4. Vet: make vet (must pass)
  5. Unit Tests: make test (all tests must pass)
  6. Build: make build (binary must compile)
  7. E2E Tests: Optional, but recommended for test changes

Code Conventions

Test Files

  • Extension: Use .go NOT _test.go
  • Location: e2e/{resource-type}/descriptive-name.go
  • Package: Match directory name (e.g., package cluster for e2e/cluster/)
  • Test Name: Format as [Suite: component] Description (e.g., [Suite: cluster] Create Cluster via API)

Example:

package cluster

var testName = "[Suite: cluster] Create Cluster via API"

Labels

Every test MUST have exactly one severity label:

import "github.com/openshift-hyperfleet/hyperfleet-e2e/pkg/labels"

var _ = ginkgo.Describe(testName,
    ginkgo.Label(labels.Tier0),  // Critical severity
    func() { ... }
)

Available labels:

  • Severity (required): Tier0, Tier1, Tier2
  • Optional: Negative, Performance, Upgrade, Disruptive, Slow

Test Structure

Required structure:

var _ = ginkgo.Describe(testName, ginkgo.Label(...), func() {
    var h *helper.Helper
    var resourceID string

    ginkgo.BeforeEach(func() {
        h = helper.New()
    })

    ginkgo.It("description", func(ctx context.Context) {
        ginkgo.By("step description")
        // test logic
    })

    ginkgo.AfterEach(func(ctx context.Context) {
        if h == nil || resourceID == "" {
            return
        }
        if err := h.CleanupTestCluster(ctx, resourceID); err != nil {
            ginkgo.GinkgoWriter.Printf("Warning: cleanup failed: %v\n", err)
        }
    })
})

Step Markers

Use ginkgo.By() for major steps ONLY. Do NOT use inside Eventually closures:

// CORRECT
ginkgo.By("waiting for cluster to become Ready")
err := h.WaitForClusterPhase(ctx, clusterID, openapi.Ready, timeout)

// INCORRECT - never do this
Eventually(func() {
    ginkgo.By("checking status")  // ❌ Wrong
    // ...
}).Should(Succeed())

Async Operations

Use Eventually with g.Expect() (not Expect()):

Eventually(func(g Gomega) {
    cluster, err := h.Client.GetCluster(ctx, clusterID)
    g.Expect(err).NotTo(HaveOccurred())
    g.Expect(cluster.Status.Phase).To(Equal(openapi.Ready))
}, timeout, pollInterval).Should(Succeed())

Resource Cleanup

ALWAYS implement cleanup in AfterEach:

ginkgo.AfterEach(func(ctx context.Context) {
    if h == nil || clusterID == "" {
        return
    }
    if err := h.CleanupTestCluster(ctx, clusterID); err != nil {
        ginkgo.GinkgoWriter.Printf("Warning: failed to cleanup cluster %s: %v\n", clusterID, err)
    }
})

Payload Templates

Test payloads in testdata/payloads/ support Go templates for dynamic values:

{
  "name": "cluster-{{.Random}}",
  "labels": {
    "created-at": "{{.Timestamp}}"
  }
}

Available variables: .Random, .Timestamp. See pkg/client/payload.go.

Boundary Statements

DO NOT

  • Modify generated code: Never edit files in pkg/api/openapi/ - these are generated by make generate
  • Use _test.go suffix: Test files must use .go extension
  • Hardcode timeouts: Use h.Cfg.Timeouts.* values from config
  • Skip cleanup: Always implement AfterEach cleanup
  • Commit without checks: Always run make check before committing
  • Use ginkgo.By() in Eventually: Only use at top-level test steps
  • Import test packages: Do NOT import e2e/* packages in production code
  • Edit OpenAPI schema: Schema is maintained in hyperfleet-api repo

DO

  • Use helper functions: Prefer h.WaitForClusterPhase() over manual polling
  • Use config values: h.Cfg.Timeouts.* for timeouts, h.Cfg.Polling.* for intervals
  • Store resource IDs: Save IDs in variables for cleanup
  • Check errors: Use Expect(err).NotTo(HaveOccurred())
  • Use context: All test functions receive context.Context

Development Workflow

Adding a New Test

  1. Create file: e2e/{suite}/descriptive-name.go
  2. Copy structure from existing test
  3. Update test name, labels, and logic
  4. Run validation checklist
  5. Test locally before submitting PR

Updating API Client

When hyperfleet-api changes:

# Default (from main branch)
make generate

# From specific branch
OPENAPI_SPEC_REF=release-0.2 make generate

# Verify changes compile
make build

Local Testing

# Build and run specific tests
make build
./bin/hyperfleet-e2e test --focus "\[Suite: cluster\]"

# Run critical tests only
./bin/hyperfleet-e2e test --label-filter=tier0

# Debug mode
./bin/hyperfleet-e2e test --log-level=debug

Configuration

Priority (highest to lowest):

  1. CLI flags: --api-url, --log-level
  2. Environment variables: HYPERFLEET_API_URL
  3. Config file: configs/config.yaml
  4. Built-in defaults

Common Patterns

Create Resource

cluster, err := h.Client.CreateClusterFromPayload(ctx, "testdata/payloads/clusters/cluster-request.json")
Expect(err).NotTo(HaveOccurred())
clusterID = *cluster.Id

Wait for Phase

err = h.WaitForClusterPhase(ctx, clusterID, openapi.Ready, h.Cfg.Timeouts.Cluster.Ready)
Expect(err).NotTo(HaveOccurred())

Verify Conditions

statuses, err := h.Client.GetClusterStatuses(ctx, clusterID)
Expect(err).NotTo(HaveOccurred())

for _, adapter := range statuses.Items {
    hasApplied := h.HasCondition(adapter.Conditions, client.ConditionTypeApplied, openapi.True)
    Expect(hasApplied).To(BeTrue())
}

Documentation