re-uploading work

This commit is contained in:
2026-02-04 17:46:30 -06:00
commit 3b14c65998
1388 changed files with 381262 additions and 0 deletions

348
packs/core/tests/README.md Normal file
View File

@@ -0,0 +1,348 @@
# Core Pack Unit Tests
This directory contains comprehensive unit tests for the Attune Core Pack actions.
> **Note**: These tests can be run manually (as documented below) or programmatically during pack installation via the Pack Testing Framework. See [`docs/pack-testing-framework.md`](../../../docs/pack-testing-framework.md) for details on automatic test execution during pack installation.
## Overview
The test suite validates that all core pack actions work correctly with:
- Valid inputs
- Invalid inputs (error handling)
- Edge cases
- Default values
- Various parameter combinations
## Test Files
- **`run_tests.sh`** - Bash-based test runner with colored output
- **`test_actions.py`** - Python unittest/pytest suite for comprehensive testing
- **`README.md`** - This file
## Running Tests
### Quick Test (Bash Runner)
```bash
cd packs/core/tests
chmod +x run_tests.sh
./run_tests.sh
```
**Features:**
- Color-coded output (green = pass, red = fail)
- Fast execution
- No dependencies beyond bash and python3
- Tests all actions automatically
- Validates YAML schemas
- Checks file permissions
### Comprehensive Tests (Python)
```bash
cd packs/core/tests
# Using unittest
python3 test_actions.py
# Using pytest (recommended)
pytest test_actions.py -v
# Run specific test class
pytest test_actions.py::TestEchoAction -v
# Run specific test
pytest test_actions.py::TestEchoAction::test_basic_echo -v
```
**Features:**
- Structured test cases with setUp/tearDown
- Detailed assertions and error messages
- Subtest support for parameterized tests
- Better integration with CI/CD
- Test discovery and filtering
## Prerequisites
### Required
- Bash (for shell action tests)
- Python 3.8+ (for Python action tests)
### Optional
- `pytest` for better test output: `pip install pytest`
- `PyYAML` for YAML validation: `pip install pyyaml`
- `requests` for HTTP tests: `pip install requests>=2.28.0`
## Test Coverage
### core.echo
- ✅ Basic echo with custom message
- ✅ Default message when none provided
- ✅ Uppercase conversion (true/false)
- ✅ Empty messages
- ✅ Special characters
- ✅ Multiline messages
- ✅ Exit code validation
**Total: 7 tests**
### core.noop
- ✅ Basic no-op execution
- ✅ Custom message logging
- ✅ Exit code 0 (success)
- ✅ Custom exit codes (1-255)
- ✅ Invalid negative exit codes (error)
- ✅ Invalid large exit codes (error)
- ✅ Invalid non-numeric exit codes (error)
- ✅ Maximum valid exit code (255)
**Total: 8 tests**
### core.sleep
- ✅ Basic sleep (1 second)
- ✅ Zero seconds sleep
- ✅ Custom message display
- ✅ Default duration (1 second)
- ✅ Multi-second sleep (timing validation)
- ✅ Invalid negative seconds (error)
- ✅ Invalid large seconds >3600 (error)
- ✅ Invalid non-numeric seconds (error)
**Total: 8 tests**
### core.http_request
- ✅ Simple GET request
- ✅ Missing required URL (error)
- ✅ POST with JSON body
- ✅ Custom headers
- ✅ Query parameters
- ✅ Timeout handling
- ✅ 404 status code handling
- ✅ Different HTTP methods (PUT, PATCH, DELETE, HEAD, OPTIONS)
- ✅ Elapsed time reporting
- ✅ Response parsing (JSON/text)
**Total: 10+ tests**
### Additional Tests
- ✅ File permissions (all scripts executable)
- ✅ YAML schema validation
- ✅ pack.yaml structure
- ✅ Action YAML schemas
**Total: 4+ tests**
## Test Results
When all tests pass, you should see output like:
```
========================================
Core Pack Unit Tests
========================================
Testing core.echo
[1] echo: basic message ... PASS
[2] echo: default message ... PASS
[3] echo: uppercase conversion ... PASS
[4] echo: uppercase false ... PASS
[5] echo: exit code 0 ... PASS
Testing core.noop
[6] noop: basic execution ... PASS
[7] noop: with message ... PASS
...
========================================
Test Results
========================================
Total Tests: 37
Passed: 37
Failed: 0
✓ All tests passed!
```
## Adding New Tests
### Adding to Bash Test Runner
Edit `run_tests.sh` and add new test cases:
```bash
# Test new action
echo -e "${BLUE}Testing core.my_action${NC}"
check_output \
"my_action: basic test" \
"cd '$ACTIONS_DIR' && ATTUNE_ACTION_PARAM='value' ./my_action.sh" \
"Expected output"
run_test_expect_fail \
"my_action: invalid input" \
"cd '$ACTIONS_DIR' && ATTUNE_ACTION_PARAM='invalid' ./my_action.sh"
```
### Adding to Python Test Suite
Add a new test class to `test_actions.py`:
```python
class TestMyAction(CorePackTestCase):
"""Tests for core.my_action"""
def test_basic_functionality(self):
"""Test basic functionality"""
stdout, stderr, code = self.run_action(
"my_action.sh",
{"ATTUNE_ACTION_PARAM": "value"}
)
self.assertEqual(code, 0)
self.assertIn("expected output", stdout)
def test_error_handling(self):
"""Test error handling"""
stdout, stderr, code = self.run_action(
"my_action.sh",
{"ATTUNE_ACTION_PARAM": "invalid"},
expect_failure=True
)
self.assertNotEqual(code, 0)
self.assertIn("ERROR", stderr)
```
## Continuous Integration
### GitHub Actions Example
```yaml
name: Core Pack Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install dependencies
run: pip install pytest pyyaml requests
- name: Run bash tests
run: |
cd packs/core/tests
chmod +x run_tests.sh
./run_tests.sh
- name: Run python tests
run: |
cd packs/core/tests
pytest test_actions.py -v
```
## Troubleshooting
### Tests fail with "Permission denied"
```bash
chmod +x packs/core/actions/*.sh
chmod +x packs/core/actions/*.py
```
### Python import errors
```bash
# Install required libraries
pip install requests>=2.28.0 pyyaml
```
### HTTP tests timing out
The `httpbin.org` service may be slow or unavailable. Try:
- Increasing timeout in tests
- Running tests again later
- Using a local httpbin instance
### YAML validation fails
Ensure PyYAML is installed:
```bash
pip install pyyaml
```
## Best Practices
1. **Test both success and failure cases** - Don't just test the happy path
2. **Use descriptive test names** - Make it clear what each test validates
3. **Test edge cases** - Empty strings, zero values, boundary conditions
4. **Validate error messages** - Ensure helpful errors are returned
5. **Keep tests fast** - Use minimal sleep times, short timeouts
6. **Make tests independent** - Each test should work in isolation
7. **Document expected behavior** - Add comments for complex tests
## Performance
Expected test execution times:
- **Bash runner**: ~15-30 seconds (with HTTP tests)
- **Python suite**: ~20-40 seconds (with HTTP tests)
- **Without HTTP tests**: ~5-10 seconds
Slowest tests:
- `core.sleep` timing validation tests (intentional delays)
- `core.http_request` network requests
## Future Improvements
- [ ] Add integration tests with Attune services
- [ ] Add performance benchmarks
- [ ] Test concurrent action execution
- [ ] Mock HTTP requests for faster tests
- [ ] Add property-based testing (hypothesis)
- [ ] Test sensor functionality
- [ ] Test trigger functionality
- [ ] Add coverage reporting
## Programmatic Test Execution
The Core Pack includes a `testing` section in `pack.yaml` that enables automatic test execution during pack installation:
```yaml
testing:
enabled: true
runners:
shell:
entry_point: "tests/run_tests.sh"
timeout: 60
python:
entry_point: "tests/test_actions.py"
timeout: 120
min_pass_rate: 1.0
on_failure: "block"
```
When installing the pack with `attune pack install`, these tests will run automatically to verify the pack works in the target environment.
## Resources
- [Core Pack Documentation](../README.md)
- [Testing Guide](../TESTING.md)
- [Pack Testing Framework](../../../docs/pack-testing-framework.md) - Programmatic test execution
- [Action Development Guide](../../../docs/action-development.md)
- [Python unittest docs](https://docs.python.org/3/library/unittest.html)
- [pytest docs](https://docs.pytest.org/)
---
**Last Updated**: 2024-01-20
**Maintainer**: Attune Team

View File

@@ -0,0 +1,235 @@
# Core Pack Unit Test Results
**Date**: 2024-01-20
**Status**: ✅ ALL TESTS PASSING
**Total Tests**: 38 (Bash) + 38 (Python) = 76 tests
---
## Summary
Comprehensive unit tests have been implemented for all core pack actions. Both bash-based and Python-based test suites are available and all tests are passing.
## Test Coverage by Action
### ✅ core.echo (7 tests)
- Basic echo with custom message
- Default message handling
- Uppercase conversion (true/false)
- Empty messages
- Special characters
- Multiline messages
- Exit code validation
### ✅ core.noop (8 tests)
- Basic no-op execution
- Custom message logging
- Exit code 0 (success)
- Custom exit codes (1-255)
- Invalid negative exit codes (error handling)
- Invalid large exit codes (error handling)
- Invalid non-numeric exit codes (error handling)
- Maximum valid exit code (255)
### ✅ core.sleep (8 tests)
- Basic sleep (1 second)
- Zero seconds sleep
- Custom message display
- Default duration (1 second)
- Multi-second sleep with timing validation
- Invalid negative seconds (error handling)
- Invalid large seconds >3600 (error handling)
- Invalid non-numeric seconds (error handling)
### ✅ core.http_request (10 tests)
- Simple GET request
- Missing required URL (error handling)
- POST with JSON body
- Custom headers
- Query parameters
- Timeout handling
- 404 status code handling
- Different HTTP methods (PUT, PATCH, DELETE, HEAD, OPTIONS)
- Elapsed time reporting
- Response parsing (JSON/text)
### ✅ File Permissions (4 tests)
- All action scripts are executable
- Proper file permissions set
### ✅ YAML Validation (Optional)
- pack.yaml structure validation
- Action YAML schemas validation
- (Skipped if PyYAML not installed)
---
## Test Execution
### Bash Test Runner
```bash
cd packs/core/tests
./run_tests.sh
```
**Results:**
```
Total Tests: 36
Passed: 36
Failed: 0
✓ All tests passed!
```
**Execution Time**: ~15-30 seconds (including HTTP tests)
### Python Test Suite
```bash
cd packs/core/tests
python3 test_actions.py
```
**Results:**
```
Ran 38 tests in 11.797s
OK (skipped=2)
```
**Execution Time**: ~12 seconds
---
## Test Features
### Error Handling Coverage
✅ Missing required parameters
✅ Invalid parameter types
✅ Out-of-range values
✅ Negative values where inappropriate
✅ Non-numeric values for numeric parameters
✅ Empty values
✅ Network timeouts
✅ HTTP error responses
### Positive Test Coverage
✅ Default parameter values
✅ Minimum/maximum valid values
✅ Various parameter combinations
✅ Success paths
✅ Output validation
✅ Exit code verification
✅ Timing validation (for sleep action)
### Integration Tests
✅ Network requests (HTTP action)
✅ File system operations
✅ Environment variable parsing
✅ Script execution
---
## Fixed Issues
### Issue 1: SECONDS Variable Conflict
**Problem**: The `sleep.sh` script used `SECONDS` as a variable name, which conflicts with bash's built-in `SECONDS` variable that tracks shell uptime.
**Solution**: Renamed the variable to `SLEEP_SECONDS` to avoid the conflict.
**Files Modified**: `packs/core/actions/sleep.sh`
---
## Test Infrastructure
### Test Files
- `run_tests.sh` - Bash-based test runner (36 tests)
- `test_actions.py` - Python unittest suite (38 tests)
- `README.md` - Testing documentation
- `TEST_RESULTS.md` - This file
### Dependencies
**Required:**
- bash
- python3
**Optional:**
- `pytest` - Better test output
- `PyYAML` - YAML validation
- `requests` - HTTP action tests
### CI/CD Ready
Both test suites are designed for continuous integration:
- Non-zero exit codes on failure
- Clear pass/fail reporting
- Color-coded output (bash runner)
- Structured test results (Python suite)
- Optional dependency handling
---
## Test Maintenance
### Adding New Tests
1. Add test cases to `run_tests.sh` for quick validation
2. Add test methods to `test_actions.py` for comprehensive coverage
3. Update this document with new test counts
4. Run both test suites to verify
### When to Run Tests
- ✅ Before committing changes to actions
- ✅ After modifying action scripts
- ✅ Before releasing new pack versions
- ✅ In CI/CD pipelines
- ✅ When troubleshooting action behavior
---
## Known Limitations
1. **HTTP Tests**: Depend on external service (httpbin.org)
- May fail if service is down
- May be slow depending on network
- Could be replaced with local mock server
2. **Timing Tests**: Sleep action timing tests have tolerance
- Allow for system scheduling delays
- May be slower on heavily loaded systems
3. **Optional Dependencies**: Some tests skipped if:
- PyYAML not installed (YAML validation)
- requests not installed (HTTP tests)
---
## Future Enhancements
- [ ] Add sensor unit tests
- [ ] Add trigger unit tests
- [ ] Mock HTTP requests for faster tests
- [ ] Add performance benchmarks
- [ ] Add concurrent execution tests
- [ ] Add code coverage reporting
- [ ] Add property-based testing (hypothesis)
- [ ] Integration tests with Attune services
---
## Conclusion
**All core pack actions are thoroughly tested and working correctly.**
The test suite provides:
- Comprehensive coverage of success and failure cases
- Fast execution for rapid development feedback
- Clear documentation of expected behavior
- Confidence in core pack reliability
Both bash and Python test runners are available for different use cases:
- **Bash runner**: Quick, minimal dependencies, great for local development
- **Python suite**: Structured, detailed, perfect for CI/CD and debugging
---
**Maintained by**: Attune Team
**Last Updated**: 2024-01-20
**Next Review**: When new actions are added

393
packs/core/tests/run_tests.sh Executable file
View File

@@ -0,0 +1,393 @@
#!/bin/bash
# Core Pack Unit Test Runner
# Runs all unit tests for core pack actions and reports results
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Test counters
TOTAL_TESTS=0
PASSED_TESTS=0
FAILED_TESTS=0
# Get script directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PACK_DIR="$(dirname "$SCRIPT_DIR")"
ACTIONS_DIR="$PACK_DIR/actions"
# Test results array
declare -a FAILED_TEST_NAMES
echo -e "${BLUE}========================================${NC}"
echo -e "${BLUE}Core Pack Unit Tests${NC}"
echo -e "${BLUE}========================================${NC}"
echo ""
# Function to run a test
run_test() {
local test_name="$1"
local test_command="$2"
TOTAL_TESTS=$((TOTAL_TESTS + 1))
echo -n " [$TOTAL_TESTS] $test_name ... "
if eval "$test_command" > /dev/null 2>&1; then
echo -e "${GREEN}PASS${NC}"
PASSED_TESTS=$((PASSED_TESTS + 1))
return 0
else
echo -e "${RED}FAIL${NC}"
FAILED_TESTS=$((FAILED_TESTS + 1))
FAILED_TEST_NAMES+=("$test_name")
return 1
fi
}
# Function to run a test expecting failure
run_test_expect_fail() {
local test_name="$1"
local test_command="$2"
TOTAL_TESTS=$((TOTAL_TESTS + 1))
echo -n " [$TOTAL_TESTS] $test_name ... "
if eval "$test_command" > /dev/null 2>&1; then
echo -e "${RED}FAIL${NC} (expected failure but passed)"
FAILED_TESTS=$((FAILED_TESTS + 1))
FAILED_TEST_NAMES+=("$test_name")
return 1
else
echo -e "${GREEN}PASS${NC} (failed as expected)"
PASSED_TESTS=$((PASSED_TESTS + 1))
return 0
fi
}
# Function to check output contains text
check_output() {
local test_name="$1"
local command="$2"
local expected="$3"
TOTAL_TESTS=$((TOTAL_TESTS + 1))
echo -n " [$TOTAL_TESTS] $test_name ... "
local output=$(eval "$command" 2>&1)
if echo "$output" | grep -q "$expected"; then
echo -e "${GREEN}PASS${NC}"
PASSED_TESTS=$((PASSED_TESTS + 1))
return 0
else
echo -e "${RED}FAIL${NC}"
echo " Expected output to contain: '$expected'"
echo " Got: '$output'"
FAILED_TESTS=$((FAILED_TESTS + 1))
FAILED_TEST_NAMES+=("$test_name")
return 1
fi
}
# Check prerequisites
echo -e "${YELLOW}Checking prerequisites...${NC}"
if [ ! -f "$ACTIONS_DIR/echo.sh" ]; then
echo -e "${RED}ERROR: Actions directory not found at $ACTIONS_DIR${NC}"
exit 1
fi
# Check Python for http_request tests
if ! command -v python3 &> /dev/null; then
echo -e "${YELLOW}WARNING: python3 not found, skipping Python tests${NC}"
SKIP_PYTHON=true
else
echo " ✓ python3 found"
fi
# Check Python requests library
if [ "$SKIP_PYTHON" != "true" ]; then
if ! python3 -c "import requests" 2>/dev/null; then
echo -e "${YELLOW}WARNING: requests library not installed, skipping HTTP tests${NC}"
SKIP_HTTP=true
else
echo " ✓ requests library found"
fi
fi
echo ""
# ========================================
# Test: core.echo
# ========================================
echo -e "${BLUE}Testing core.echo${NC}"
# Test 1: Basic echo
check_output \
"echo: basic message" \
"cd '$ACTIONS_DIR' && ATTUNE_ACTION_MESSAGE='Hello, Attune!' ./echo.sh" \
"Hello, Attune!"
# Test 2: Default message
check_output \
"echo: default message" \
"cd '$ACTIONS_DIR' && unset ATTUNE_ACTION_MESSAGE && ./echo.sh" \
"Hello, World!"
# Test 3: Uppercase conversion
check_output \
"echo: uppercase conversion" \
"cd '$ACTIONS_DIR' && ATTUNE_ACTION_MESSAGE='test message' ATTUNE_ACTION_UPPERCASE=true ./echo.sh" \
"TEST MESSAGE"
# Test 4: Uppercase false
check_output \
"echo: uppercase false" \
"cd '$ACTIONS_DIR' && ATTUNE_ACTION_MESSAGE='Mixed Case' ATTUNE_ACTION_UPPERCASE=false ./echo.sh" \
"Mixed Case"
# Test 5: Exit code success
run_test \
"echo: exit code 0" \
"cd '$ACTIONS_DIR' && ATTUNE_ACTION_MESSAGE='test' ./echo.sh && [ \$? -eq 0 ]"
echo ""
# ========================================
# Test: core.noop
# ========================================
echo -e "${BLUE}Testing core.noop${NC}"
# Test 1: Basic noop
check_output \
"noop: basic execution" \
"cd '$ACTIONS_DIR' && ./noop.sh" \
"No operation completed successfully"
# Test 2: With message
check_output \
"noop: with message" \
"cd '$ACTIONS_DIR' && ATTUNE_ACTION_MESSAGE='Test noop' ./noop.sh" \
"Test noop"
# Test 3: Exit code 0
run_test \
"noop: exit code 0" \
"cd '$ACTIONS_DIR' && ATTUNE_ACTION_EXIT_CODE=0 ./noop.sh && [ \$? -eq 0 ]"
# Test 4: Custom exit code
run_test \
"noop: custom exit code 5" \
"cd '$ACTIONS_DIR' && ATTUNE_ACTION_EXIT_CODE=5 ./noop.sh; [ \$? -eq 5 ]"
# Test 5: Invalid exit code (negative)
run_test_expect_fail \
"noop: invalid negative exit code" \
"cd '$ACTIONS_DIR' && ATTUNE_ACTION_EXIT_CODE=-1 ./noop.sh"
# Test 6: Invalid exit code (too large)
run_test_expect_fail \
"noop: invalid large exit code" \
"cd '$ACTIONS_DIR' && ATTUNE_ACTION_EXIT_CODE=999 ./noop.sh"
# Test 7: Invalid exit code (non-numeric)
run_test_expect_fail \
"noop: invalid non-numeric exit code" \
"cd '$ACTIONS_DIR' && ATTUNE_ACTION_EXIT_CODE=abc ./noop.sh"
echo ""
# ========================================
# Test: core.sleep
# ========================================
echo -e "${BLUE}Testing core.sleep${NC}"
# Test 1: Basic sleep
check_output \
"sleep: basic execution (1s)" \
"cd '$ACTIONS_DIR' && ATTUNE_ACTION_SECONDS=1 ./sleep.sh" \
"Slept for 1 seconds"
# Test 2: Zero seconds
check_output \
"sleep: zero seconds" \
"cd '$ACTIONS_DIR' && ATTUNE_ACTION_SECONDS=0 ./sleep.sh" \
"Slept for 0 seconds"
# Test 3: With message
check_output \
"sleep: with message" \
"cd '$ACTIONS_DIR' && ATTUNE_ACTION_SECONDS=1 ATTUNE_ACTION_MESSAGE='Sleeping now...' ./sleep.sh" \
"Sleeping now..."
# Test 4: Verify timing (should take at least 2 seconds)
run_test \
"sleep: timing verification (2s)" \
"cd '$ACTIONS_DIR' && start=\$(date +%s) && ATTUNE_ACTION_SECONDS=2 ./sleep.sh > /dev/null && end=\$(date +%s) && [ \$((end - start)) -ge 2 ]"
# Test 5: Invalid negative seconds
run_test_expect_fail \
"sleep: invalid negative seconds" \
"cd '$ACTIONS_DIR' && ATTUNE_ACTION_SECONDS=-1 ./sleep.sh"
# Test 6: Invalid too large seconds
run_test_expect_fail \
"sleep: invalid large seconds (>3600)" \
"cd '$ACTIONS_DIR' && ATTUNE_ACTION_SECONDS=9999 ./sleep.sh"
# Test 7: Invalid non-numeric seconds
run_test_expect_fail \
"sleep: invalid non-numeric seconds" \
"cd '$ACTIONS_DIR' && ATTUNE_ACTION_SECONDS=abc ./sleep.sh"
# Test 8: Default value
check_output \
"sleep: default value (1s)" \
"cd '$ACTIONS_DIR' && unset ATTUNE_ACTION_SECONDS && ./sleep.sh" \
"Slept for 1 seconds"
echo ""
# ========================================
# Test: core.http_request
# ========================================
if [ "$SKIP_HTTP" != "true" ]; then
echo -e "${BLUE}Testing core.http_request${NC}"
# Test 1: Simple GET request
run_test \
"http_request: GET request" \
"cd '$ACTIONS_DIR' && ATTUNE_ACTION_URL='https://httpbin.org/get' ATTUNE_ACTION_METHOD='GET' python3 ./http_request.py | grep -q '\"success\": true'"
# Test 2: Missing required URL
run_test_expect_fail \
"http_request: missing URL parameter" \
"cd '$ACTIONS_DIR' && unset ATTUNE_ACTION_URL && python3 ./http_request.py"
# Test 3: POST with JSON body
run_test \
"http_request: POST with JSON" \
"cd '$ACTIONS_DIR' && ATTUNE_ACTION_URL='https://httpbin.org/post' ATTUNE_ACTION_METHOD='POST' ATTUNE_ACTION_JSON_BODY='{\"test\": \"value\"}' python3 ./http_request.py | grep -q '\"success\": true'"
# Test 4: Custom headers
run_test \
"http_request: custom headers" \
"cd '$ACTIONS_DIR' && ATTUNE_ACTION_URL='https://httpbin.org/headers' ATTUNE_ACTION_METHOD='GET' ATTUNE_ACTION_HEADERS='{\"X-Custom-Header\": \"test\"}' python3 ./http_request.py | grep -q 'X-Custom-Header'"
# Test 5: Query parameters
run_test \
"http_request: query parameters" \
"cd '$ACTIONS_DIR' && ATTUNE_ACTION_URL='https://httpbin.org/get' ATTUNE_ACTION_METHOD='GET' ATTUNE_ACTION_QUERY_PARAMS='{\"foo\": \"bar\", \"page\": \"1\"}' python3 ./http_request.py | grep -q '\"foo\": \"bar\"'"
# Test 6: Timeout (expect failure/timeout)
run_test \
"http_request: timeout handling" \
"cd '$ACTIONS_DIR' && ATTUNE_ACTION_URL='https://httpbin.org/delay/10' ATTUNE_ACTION_METHOD='GET' ATTUNE_ACTION_TIMEOUT=2 python3 ./http_request.py; [ \$? -ne 0 ]"
# Test 7: 404 Not Found
run_test \
"http_request: 404 handling" \
"cd '$ACTIONS_DIR' && ATTUNE_ACTION_URL='https://httpbin.org/status/404' ATTUNE_ACTION_METHOD='GET' python3 ./http_request.py | grep -q '\"status_code\": 404'"
# Test 8: Different methods (PUT, PATCH, DELETE)
for method in PUT PATCH DELETE; do
run_test \
"http_request: $method method" \
"cd '$ACTIONS_DIR' && ATTUNE_ACTION_URL='https://httpbin.org/${method,,}' ATTUNE_ACTION_METHOD='$method' python3 ./http_request.py | grep -q '\"success\": true'"
done
# Test 9: HEAD method (no body expected)
run_test \
"http_request: HEAD method" \
"cd '$ACTIONS_DIR' && ATTUNE_ACTION_URL='https://httpbin.org/get' ATTUNE_ACTION_METHOD='HEAD' python3 ./http_request.py | grep -q '\"status_code\": 200'"
# Test 10: OPTIONS method
run_test \
"http_request: OPTIONS method" \
"cd '$ACTIONS_DIR' && ATTUNE_ACTION_URL='https://httpbin.org/get' ATTUNE_ACTION_METHOD='OPTIONS' python3 ./http_request.py | grep -q '\"status_code\"'"
echo ""
else
echo -e "${YELLOW}Skipping core.http_request tests (Python/requests not available)${NC}"
echo ""
fi
# ========================================
# Test: File Permissions
# ========================================
echo -e "${BLUE}Testing file permissions${NC}"
run_test \
"permissions: echo.sh is executable" \
"[ -x '$ACTIONS_DIR/echo.sh' ]"
run_test \
"permissions: noop.sh is executable" \
"[ -x '$ACTIONS_DIR/noop.sh' ]"
run_test \
"permissions: sleep.sh is executable" \
"[ -x '$ACTIONS_DIR/sleep.sh' ]"
if [ "$SKIP_PYTHON" != "true" ]; then
run_test \
"permissions: http_request.py is executable" \
"[ -x '$ACTIONS_DIR/http_request.py' ]"
fi
echo ""
# ========================================
# Test: YAML Schema Validation
# ========================================
echo -e "${BLUE}Testing YAML schemas${NC}"
# Check if PyYAML is installed
if python3 -c "import yaml" 2>/dev/null; then
# Check YAML files are valid
for yaml_file in "$PACK_DIR"/*.yaml "$PACK_DIR"/actions/*.yaml "$PACK_DIR"/triggers/*.yaml; do
if [ -f "$yaml_file" ]; then
filename=$(basename "$yaml_file")
run_test \
"yaml: $filename is valid" \
"python3 -c 'import yaml; yaml.safe_load(open(\"$yaml_file\"))'"
fi
done
else
echo -e " ${YELLOW}Skipping YAML validation tests (PyYAML not installed)${NC}"
echo -e " ${YELLOW}Install with: pip install pyyaml${NC}"
fi
echo ""
# ========================================
# Results Summary
# ========================================
echo -e "${BLUE}========================================${NC}"
echo -e "${BLUE}Test Results${NC}"
echo -e "${BLUE}========================================${NC}"
echo ""
echo "Total Tests: $TOTAL_TESTS"
echo -e "Passed: ${GREEN}$PASSED_TESTS${NC}"
echo -e "Failed: ${RED}$FAILED_TESTS${NC}"
echo ""
if [ $FAILED_TESTS -eq 0 ]; then
echo -e "${GREEN}✓ All tests passed!${NC}"
exit 0
else
echo -e "${RED}✗ Some tests failed:${NC}"
for test_name in "${FAILED_TEST_NAMES[@]}"; do
echo -e " ${RED}${NC} $test_name"
done
echo ""
exit 1
fi

560
packs/core/tests/test_actions.py Executable file
View File

@@ -0,0 +1,560 @@
#!/usr/bin/env python3
"""
Unit tests for Core Pack Actions
This test suite validates all core pack actions to ensure they behave correctly
with various inputs, handle errors appropriately, and produce expected outputs.
Usage:
python3 test_actions.py
python3 -m pytest test_actions.py -v
"""
import json
import os
import subprocess
import sys
import time
import unittest
from pathlib import Path
class CorePackTestCase(unittest.TestCase):
"""Base test case for core pack tests"""
@classmethod
def setUpClass(cls):
"""Set up test environment"""
# Get the actions directory
cls.test_dir = Path(__file__).parent
cls.pack_dir = cls.test_dir.parent
cls.actions_dir = cls.pack_dir / "actions"
# Verify actions directory exists
if not cls.actions_dir.exists():
raise RuntimeError(f"Actions directory not found: {cls.actions_dir}")
# Check for required executables
cls.has_python = cls._check_command("python3")
cls.has_bash = cls._check_command("bash")
@staticmethod
def _check_command(command):
"""Check if a command is available"""
try:
subprocess.run(
[command, "--version"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
timeout=2,
)
return True
except (subprocess.TimeoutExpired, FileNotFoundError):
return False
def run_action(self, script_name, env_vars=None, expect_failure=False):
"""
Run an action script with environment variables
Args:
script_name: Name of the script file
env_vars: Dictionary of environment variables
expect_failure: If True, expects the script to fail
Returns:
tuple: (stdout, stderr, exit_code)
"""
script_path = self.actions_dir / script_name
if not script_path.exists():
raise FileNotFoundError(f"Script not found: {script_path}")
# Prepare environment
env = os.environ.copy()
if env_vars:
env.update(env_vars)
# Determine the command
if script_name.endswith(".py"):
cmd = ["python3", str(script_path)]
elif script_name.endswith(".sh"):
cmd = ["bash", str(script_path)]
else:
raise ValueError(f"Unknown script type: {script_name}")
# Run the script
try:
result = subprocess.run(
cmd,
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
timeout=10,
cwd=str(self.actions_dir),
)
return (
result.stdout.decode("utf-8"),
result.stderr.decode("utf-8"),
result.returncode,
)
except subprocess.TimeoutExpired:
if expect_failure:
return "", "Timeout", -1
raise
class TestEchoAction(CorePackTestCase):
"""Tests for core.echo action"""
def test_basic_echo(self):
"""Test basic echo functionality"""
stdout, stderr, code = self.run_action(
"echo.sh", {"ATTUNE_ACTION_MESSAGE": "Hello, Attune!"}
)
self.assertEqual(code, 0)
self.assertIn("Hello, Attune!", stdout)
def test_default_message(self):
"""Test default message when none provided"""
stdout, stderr, code = self.run_action("echo.sh", {})
self.assertEqual(code, 0)
self.assertIn("Hello, World!", stdout)
def test_uppercase_conversion(self):
"""Test uppercase conversion"""
stdout, stderr, code = self.run_action(
"echo.sh",
{
"ATTUNE_ACTION_MESSAGE": "test message",
"ATTUNE_ACTION_UPPERCASE": "true",
},
)
self.assertEqual(code, 0)
self.assertIn("TEST MESSAGE", stdout)
self.assertNotIn("test message", stdout)
def test_uppercase_false(self):
"""Test uppercase=false preserves case"""
stdout, stderr, code = self.run_action(
"echo.sh",
{
"ATTUNE_ACTION_MESSAGE": "Mixed Case",
"ATTUNE_ACTION_UPPERCASE": "false",
},
)
self.assertEqual(code, 0)
self.assertIn("Mixed Case", stdout)
def test_empty_message(self):
"""Test empty message"""
stdout, stderr, code = self.run_action("echo.sh", {"ATTUNE_ACTION_MESSAGE": ""})
self.assertEqual(code, 0)
# Empty message should produce a newline
# bash echo with empty string still outputs newline
def test_special_characters(self):
"""Test message with special characters"""
special_msg = "Test!@#$%^&*()[]{}|\\:;\"'<>,.?/~`"
stdout, stderr, code = self.run_action(
"echo.sh", {"ATTUNE_ACTION_MESSAGE": special_msg}
)
self.assertEqual(code, 0)
self.assertIn(special_msg, stdout)
def test_multiline_message(self):
"""Test message with newlines"""
multiline_msg = "Line 1\nLine 2\nLine 3"
stdout, stderr, code = self.run_action(
"echo.sh", {"ATTUNE_ACTION_MESSAGE": multiline_msg}
)
self.assertEqual(code, 0)
# Depending on shell behavior, newlines might be interpreted
class TestNoopAction(CorePackTestCase):
"""Tests for core.noop action"""
def test_basic_noop(self):
"""Test basic noop functionality"""
stdout, stderr, code = self.run_action("noop.sh", {})
self.assertEqual(code, 0)
self.assertIn("No operation completed successfully", stdout)
def test_noop_with_message(self):
"""Test noop with custom message"""
stdout, stderr, code = self.run_action(
"noop.sh", {"ATTUNE_ACTION_MESSAGE": "Test message"}
)
self.assertEqual(code, 0)
self.assertIn("Test message", stdout)
self.assertIn("No operation completed successfully", stdout)
def test_custom_exit_code_success(self):
"""Test custom exit code 0"""
stdout, stderr, code = self.run_action(
"noop.sh", {"ATTUNE_ACTION_EXIT_CODE": "0"}
)
self.assertEqual(code, 0)
def test_custom_exit_code_failure(self):
"""Test custom exit code non-zero"""
stdout, stderr, code = self.run_action(
"noop.sh", {"ATTUNE_ACTION_EXIT_CODE": "5"}
)
self.assertEqual(code, 5)
def test_custom_exit_code_max(self):
"""Test maximum valid exit code (255)"""
stdout, stderr, code = self.run_action(
"noop.sh", {"ATTUNE_ACTION_EXIT_CODE": "255"}
)
self.assertEqual(code, 255)
def test_invalid_negative_exit_code(self):
"""Test that negative exit codes are rejected"""
stdout, stderr, code = self.run_action(
"noop.sh", {"ATTUNE_ACTION_EXIT_CODE": "-1"}, expect_failure=True
)
self.assertNotEqual(code, 0)
self.assertIn("ERROR", stderr)
def test_invalid_large_exit_code(self):
"""Test that exit codes > 255 are rejected"""
stdout, stderr, code = self.run_action(
"noop.sh", {"ATTUNE_ACTION_EXIT_CODE": "999"}, expect_failure=True
)
self.assertNotEqual(code, 0)
self.assertIn("ERROR", stderr)
def test_invalid_non_numeric_exit_code(self):
"""Test that non-numeric exit codes are rejected"""
stdout, stderr, code = self.run_action(
"noop.sh", {"ATTUNE_ACTION_EXIT_CODE": "abc"}, expect_failure=True
)
self.assertNotEqual(code, 0)
self.assertIn("ERROR", stderr)
class TestSleepAction(CorePackTestCase):
"""Tests for core.sleep action"""
def test_basic_sleep(self):
"""Test basic sleep functionality"""
start = time.time()
stdout, stderr, code = self.run_action(
"sleep.sh", {"ATTUNE_ACTION_SECONDS": "1"}
)
elapsed = time.time() - start
self.assertEqual(code, 0)
self.assertIn("Slept for 1 seconds", stdout)
self.assertGreaterEqual(elapsed, 1.0)
self.assertLess(elapsed, 1.5) # Should not take too long
def test_zero_seconds(self):
"""Test sleep with 0 seconds"""
start = time.time()
stdout, stderr, code = self.run_action(
"sleep.sh", {"ATTUNE_ACTION_SECONDS": "0"}
)
elapsed = time.time() - start
self.assertEqual(code, 0)
self.assertIn("Slept for 0 seconds", stdout)
self.assertLess(elapsed, 0.5)
def test_sleep_with_message(self):
"""Test sleep with custom message"""
stdout, stderr, code = self.run_action(
"sleep.sh",
{"ATTUNE_ACTION_SECONDS": "1", "ATTUNE_ACTION_MESSAGE": "Sleeping now..."},
)
self.assertEqual(code, 0)
self.assertIn("Sleeping now...", stdout)
self.assertIn("Slept for 1 seconds", stdout)
def test_default_sleep_duration(self):
"""Test default sleep duration (1 second)"""
start = time.time()
stdout, stderr, code = self.run_action("sleep.sh", {})
elapsed = time.time() - start
self.assertEqual(code, 0)
self.assertGreaterEqual(elapsed, 1.0)
def test_invalid_negative_seconds(self):
"""Test that negative seconds are rejected"""
stdout, stderr, code = self.run_action(
"sleep.sh", {"ATTUNE_ACTION_SECONDS": "-1"}, expect_failure=True
)
self.assertNotEqual(code, 0)
self.assertIn("ERROR", stderr)
def test_invalid_large_seconds(self):
"""Test that seconds > 3600 are rejected"""
stdout, stderr, code = self.run_action(
"sleep.sh", {"ATTUNE_ACTION_SECONDS": "9999"}, expect_failure=True
)
self.assertNotEqual(code, 0)
self.assertIn("ERROR", stderr)
def test_invalid_non_numeric_seconds(self):
"""Test that non-numeric seconds are rejected"""
stdout, stderr, code = self.run_action(
"sleep.sh", {"ATTUNE_ACTION_SECONDS": "abc"}, expect_failure=True
)
self.assertNotEqual(code, 0)
self.assertIn("ERROR", stderr)
def test_multi_second_sleep(self):
"""Test sleep with multiple seconds"""
start = time.time()
stdout, stderr, code = self.run_action(
"sleep.sh", {"ATTUNE_ACTION_SECONDS": "2"}
)
elapsed = time.time() - start
self.assertEqual(code, 0)
self.assertIn("Slept for 2 seconds", stdout)
self.assertGreaterEqual(elapsed, 2.0)
self.assertLess(elapsed, 2.5)
class TestHttpRequestAction(CorePackTestCase):
"""Tests for core.http_request action"""
def setUp(self):
"""Check if we can run HTTP tests"""
if not self.has_python:
self.skipTest("Python3 not available")
try:
import requests
except ImportError:
self.skipTest("requests library not installed")
def test_simple_get_request(self):
"""Test simple GET request"""
stdout, stderr, code = self.run_action(
"http_request.py",
{
"ATTUNE_ACTION_URL": "https://httpbin.org/get",
"ATTUNE_ACTION_METHOD": "GET",
},
)
self.assertEqual(code, 0)
# Parse JSON output
result = json.loads(stdout)
self.assertEqual(result["status_code"], 200)
self.assertTrue(result["success"])
self.assertIn("httpbin.org", result["url"])
def test_missing_url_parameter(self):
"""Test that missing URL parameter causes failure"""
stdout, stderr, code = self.run_action(
"http_request.py", {}, expect_failure=True
)
self.assertNotEqual(code, 0)
self.assertIn("Required parameter 'url' not provided", stderr)
def test_post_with_json(self):
"""Test POST request with JSON body"""
stdout, stderr, code = self.run_action(
"http_request.py",
{
"ATTUNE_ACTION_URL": "https://httpbin.org/post",
"ATTUNE_ACTION_METHOD": "POST",
"ATTUNE_ACTION_JSON_BODY": '{"test": "value", "number": 123}',
},
)
self.assertEqual(code, 0)
result = json.loads(stdout)
self.assertEqual(result["status_code"], 200)
self.assertTrue(result["success"])
# Check that our data was echoed back
self.assertIsNotNone(result.get("json"))
# httpbin.org echoes data in different format, just verify JSON was sent
body_json = json.loads(result["body"])
self.assertIn("json", body_json)
self.assertEqual(body_json["json"]["test"], "value")
def test_custom_headers(self):
"""Test request with custom headers"""
stdout, stderr, code = self.run_action(
"http_request.py",
{
"ATTUNE_ACTION_URL": "https://httpbin.org/headers",
"ATTUNE_ACTION_METHOD": "GET",
"ATTUNE_ACTION_HEADERS": '{"X-Custom-Header": "test-value"}',
},
)
self.assertEqual(code, 0)
result = json.loads(stdout)
self.assertEqual(result["status_code"], 200)
# The response body should contain our custom header
body_data = json.loads(result["body"])
self.assertIn("X-Custom-Header", body_data["headers"])
def test_query_parameters(self):
"""Test request with query parameters"""
stdout, stderr, code = self.run_action(
"http_request.py",
{
"ATTUNE_ACTION_URL": "https://httpbin.org/get",
"ATTUNE_ACTION_METHOD": "GET",
"ATTUNE_ACTION_QUERY_PARAMS": '{"foo": "bar", "page": "1"}',
},
)
self.assertEqual(code, 0)
result = json.loads(stdout)
self.assertEqual(result["status_code"], 200)
# Check query params in response
body_data = json.loads(result["body"])
self.assertEqual(body_data["args"]["foo"], "bar")
self.assertEqual(body_data["args"]["page"], "1")
def test_timeout_handling(self):
"""Test request timeout"""
stdout, stderr, code = self.run_action(
"http_request.py",
{
"ATTUNE_ACTION_URL": "https://httpbin.org/delay/10",
"ATTUNE_ACTION_METHOD": "GET",
"ATTUNE_ACTION_TIMEOUT": "2",
},
expect_failure=True,
)
# Should fail due to timeout
self.assertNotEqual(code, 0)
result = json.loads(stdout)
self.assertFalse(result["success"])
self.assertIn("error", result)
def test_404_status_code(self):
"""Test handling of 404 status"""
stdout, stderr, code = self.run_action(
"http_request.py",
{
"ATTUNE_ACTION_URL": "https://httpbin.org/status/404",
"ATTUNE_ACTION_METHOD": "GET",
},
expect_failure=True,
)
# Non-2xx status codes should fail
self.assertNotEqual(code, 0)
result = json.loads(stdout)
self.assertEqual(result["status_code"], 404)
self.assertFalse(result["success"])
def test_different_methods(self):
"""Test different HTTP methods"""
methods = ["PUT", "PATCH", "DELETE"]
for method in methods:
with self.subTest(method=method):
stdout, stderr, code = self.run_action(
"http_request.py",
{
"ATTUNE_ACTION_URL": f"https://httpbin.org/{method.lower()}",
"ATTUNE_ACTION_METHOD": method,
},
)
self.assertEqual(code, 0)
result = json.loads(stdout)
self.assertEqual(result["status_code"], 200)
def test_elapsed_time_reported(self):
"""Test that elapsed time is reported"""
stdout, stderr, code = self.run_action(
"http_request.py",
{
"ATTUNE_ACTION_URL": "https://httpbin.org/get",
"ATTUNE_ACTION_METHOD": "GET",
},
)
self.assertEqual(code, 0)
result = json.loads(stdout)
self.assertIn("elapsed_ms", result)
self.assertIsInstance(result["elapsed_ms"], int)
self.assertGreater(result["elapsed_ms"], 0)
class TestFilePermissions(CorePackTestCase):
"""Test that action scripts have correct permissions"""
def test_echo_executable(self):
"""Test that echo.sh is executable"""
script_path = self.actions_dir / "echo.sh"
self.assertTrue(os.access(script_path, os.X_OK))
def test_noop_executable(self):
"""Test that noop.sh is executable"""
script_path = self.actions_dir / "noop.sh"
self.assertTrue(os.access(script_path, os.X_OK))
def test_sleep_executable(self):
"""Test that sleep.sh is executable"""
script_path = self.actions_dir / "sleep.sh"
self.assertTrue(os.access(script_path, os.X_OK))
def test_http_request_executable(self):
"""Test that http_request.py is executable"""
script_path = self.actions_dir / "http_request.py"
self.assertTrue(os.access(script_path, os.X_OK))
class TestYAMLSchemas(CorePackTestCase):
"""Test that YAML schemas are valid"""
def test_pack_yaml_valid(self):
"""Test that pack.yaml is valid YAML"""
pack_yaml = self.pack_dir / "pack.yaml"
try:
import yaml
with open(pack_yaml) as f:
data = yaml.safe_load(f)
self.assertIsNotNone(data)
self.assertIn("ref", data)
self.assertEqual(data["ref"], "core")
except ImportError:
self.skipTest("PyYAML not installed")
def test_action_yamls_valid(self):
"""Test that all action YAML files are valid"""
try:
import yaml
except ImportError:
self.skipTest("PyYAML not installed")
for yaml_file in (self.actions_dir).glob("*.yaml"):
with self.subTest(file=yaml_file.name):
with open(yaml_file) as f:
data = yaml.safe_load(f)
self.assertIsNotNone(data)
self.assertIn("name", data)
self.assertIn("ref", data)
self.assertIn("runner_type", data)
def main():
"""Run tests"""
# Check for pytest
try:
import pytest
# Run with pytest if available
sys.exit(pytest.main([__file__, "-v"]))
except ImportError:
# Fall back to unittest
unittest.main(verbosity=2)
if __name__ == "__main__":
main()