Use when writing acceptance tests for Chariot APIs, creating test operations that abstract HTTP details, or understanding the TestHelper pattern for business-level test abstractions in pkg/ops/.
Use this skill when:
You MUST use TodoWrite before starting to track all workflow steps when implementing or adding operations.
Operations represent user actions at a business level (e.g., "Add Asset", "Delete Seed") and hide:
// ❌ Without operations (raw HTTP details exposed)
body := map[string]any{"type": "asset", "group": "example.com", "identifier": "example.com"}
resp, err := client.Post("/asset", body)
if err != nil { /* handle */ }
var wrappers []registry.Wrapper[model.Assetlike]
json.Unmarshal(resp.Body, &wrappers)
asset := wrappers[0].Model
asset.GetBase().Username = user.Email // Manual hydration
// ✅ With operations (business-level abstraction)
asset, err := helper.AddAsset(data)
type TestHelper struct {
User users.User // Cognito test user
Client *api.Client // Authenticated API client
Plextrac *plextrac.Manager // PlexTrac integration client
// Embedded data factory (methods promoted to TestHelper)
*data.ModelDataFactory
// Embedded assertors (methods promoted to TestHelper)
*queue.QueueAssertor
*data2.DataAssertor
*table.TableAssertor
*secrets.SecretsAssertor
*files.FilesAssertor
*users2.UsersAssertor
*api2.APIAssertor
}
Key components:
AddAsset())Two helper types:
// 1. NewTestHelper - Active monitoring (for API tests)
helper, err := ops.NewTestHelper(t)
// 2. NewPassiveTestHelper - No monitoring (for compute tests)
helper, err := ops.NewPassiveTestHelper()
When to use:
Most common - applies to Assets, Seeds, Risks:
// CREATE
helper.AddAsset(modelData)
helper.AddSeed(modelData)
helper.AddRisk(modelData, target)
// READ
helper.GetAsset(modelData)
helper.GetSeed(modelData)
helper.GetRisk(modelData)
// UPDATE
helper.UpdateAsset(modelData)
helper.UpdateSeed(modelData)
helper.UpdateRisk(modelData)
// DELETE
helper.DeleteAsset(modelData)
helper.DeleteSeed(modelData)
helper.DeleteRisk(modelData)
See references/patterns.md for complete implementations.
Multi-tenant testing:
helper.AddAccount(account) // Add user to account
helper.GetAccount(account) // Get accounts for user
helper.DeleteAccount(account) // Remove user from account
File upload/download with S3:
helper.AddFile(file) // Simple upload
helper.AddFileMultipart(file) // Large files (parallel chunks)
helper.UpdateFile(file) // Re-upload
helper.DeleteFile(file) // Remove
helper.GetFileDownloadURL(name) // Get presigned URL
helper.DownloadFile(name) // Download content
Async job management:
helper.CreateJobs(request) // Create jobs with optional filter
helper.DeleteJob(jobKey) // Cancel/delete job
helper.GetJob(jobKey) // Retrieve job status
See references/patterns.md for detailed implementations of all patterns.
All operations follow five shared patterns:
| Pattern | Purpose | Example |
|---|---|---|
| A. Type-Safe Requests | Automatic unmarshaling | web.Request[T](...) |
| B. Error Wrapping | Contextual error messages | fmt.Errorf("...: %w", err) |
| C. Optional Fields | Conditional inclusion | if field != "" { body["field"] = field } |
| D. Response Validation | Early error detection | if len(results) == 0 { return error } |
| E. Field Hydration | Complete test data | model.Username = h.Username |
// Generic request with automatic unmarshaling
result, err := web.Request[ResponseType](
h.Client, // Authenticated client
"METHOD", // HTTP method
"/path", // API endpoint
body, // Request payload
)
response := result.Body // Already typed as ResponseType
result, err := web.Request[T](h.Client, "POST", "/endpoint", body)
if err != nil {
return nil, fmt.Errorf("failed to perform operation: %w", err)
}
body := map[string]any{
"required": modelData.Required,
}
// Conditionally include optional fields
if modelData.Comment != "" {
body["comment"] = modelData.Comment
}
assets := result.Body.Assetlikes()
// Validate before returning
if len(assets) == 0 {
return nil, fmt.Errorf("asset results are empty")
}
if len(assets) > 1 {
return nil, fmt.Errorf("multiple asset results found")
}
return assets[0], nil
asset := result.Body[0].Model
// Set fields not returned by API
asset.GetBase().Username = h.Username
return asset, nil
See references/common-patterns.md for complete pattern details and examples.
Operations are organized by domain in pkg/ops/:
pkg/ops/
├── helper.go # TestHelper struct, initialization
├── assets.go # Asset CRUD operations
├── seeds.go # Seed CRUD operations
├── risks.go # Risk CRUD operations
├── accounts.go # Account management
├── files.go # File upload/download
├── jobs.go # Job creation/management
├── users.go # User operations
├── capabilities.go # Capability operations
├── export.go # Data export
├── plextrac.go # PlexTrac integration
└── async.go # Async helpers
Organization principles:
See references/organization.md for complete structure details.
Does operation fit existing domain?
├─ YES → Add to existing file (assets.go, seeds.go, etc.)
└─ NO → Create new domain file (mynewdomain.go)
func (h *TestHelper) MyOperation(input MyInputType) (MyReturnType, error) {
// 1. Build request body
body := map[string]any{
"requiredField": input.RequiredField,
}
// 2. Handle optional fields (Pattern C)
if input.OptionalField != "" {
body["optionalField"] = input.OptionalField
}
// 3. Make type-safe request (Pattern A)
result, err := web.Request[MyResponseType](
h.Client, "POST", "/my/endpoint", body)
// 4. Wrap errors (Pattern B)
if err != nil {
return nil, fmt.Errorf("failed to perform operation: %w", err)
}
// 5. Validate response (Pattern D)
response := result.Body
if !response.IsValid() {
return nil, fmt.Errorf("invalid response from API")
}
// 6. Hydrate fields (Pattern E)
response.SomeField = h.User.Email
return response, nil
}
func Test_MyOperation(t *testing.T) {
t.Parallel()
// Initialize helper
helper, err := ops.NewTestHelper(t)
require.NoError(t, err)
// Generate test data
input := MyInputType{RequiredField: "value"}
// Call operation
result, err := helper.MyOperation(input)
require.NoError(t, err)
// Validate
assert.NotNil(t, result)
helper.AssertTableItemInserted(t, result)
}
See references/adding-operations.md for complete step-by-step guide.
func Test_AssetWorkflow(t *testing.T) {
t.Parallel()
// 1. Initialize helper
helper, err := ops.NewTestHelper(t)
require.NoError(t, err)
// 2. Generate test data
asset := helper.GenerateDomainAssetData()
// 3. CREATE
created, err := helper.AddAsset(asset)
require.NoError(t, err)
// 4. Validate creation
helper.ValidateAsset(t, created, asset)
// 5. READ
queried, err := helper.GetAsset(created)
require.NoError(t, err)
// 6. UPDATE
queried.SetStatus(model.Frozen)
updated, err := helper.UpdateAsset(queried)
require.NoError(t, err)
// 7. Assert async effects
helper.AssertJobsQueuedForTarget(t, updated)
// 8. DELETE
deleted, err := helper.DeleteAsset(updated)
require.NoError(t, err)
// 9. Automatic cleanup via t.Cleanup
}
AddAsset (clear) vs CreateAsset (generic)(model.Asset, error) vs (interface{}, error)fmt.Errorf("failed: %w", err) preserves chain*http.ResponseSee references/best-practices.md for complete guide with examples.
| Need | Pattern | Example |
|---|---|---|
| API call | Type-Safe Request | web.Request[T](h.Client, "POST", "/path", body) |
| Error handling | Error Wrapping | fmt.Errorf("failed to add: %w", err) |
| Non-required fields | Optional Fields | if field != "" { body["field"] = field } |
| Check API response | Response Validation | if len(results) == 0 { return error } |
| Missing API fields | Field Hydration | model.Username = h.Username |
| Resource | CREATE | READ | UPDATE | DELETE |
|---|---|---|---|---|
| Assets | AddAsset | GetAsset | UpdateAsset | DeleteAsset |
| Seeds | AddSeed | GetSeed | UpdateSeed | DeleteSeed |
| Risks | AddRisk | GetRisk | UpdateRisk | DeleteRisk |
| Files | AddFile | DownloadFile | UpdateFile | DeleteFile |
| Jobs | CreateJobs | GetJob | - | DeleteJob |
Operations Package Purpose:
Key Patterns:
web.Request[T]()%wIntegration:
helper.AddAsset(data)Detailed guides:
Quick links: