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

361
packs/core/README.md Normal file
View File

@@ -0,0 +1,361 @@
# Attune Core Pack
The **Core Pack** is the foundational system pack for Attune, providing essential automation components including timer triggers, HTTP utilities, and basic shell actions.
## Overview
The core pack is automatically installed with Attune and provides the building blocks for creating automation workflows. It includes:
- **Timer Triggers**: Interval-based, cron-based, and one-shot datetime timers
- **HTTP Actions**: Make HTTP requests to external APIs
- **Shell Actions**: Execute basic shell commands (echo, sleep, noop)
- **Built-in Sensors**: System sensors for monitoring time-based events
## Components
### Actions
#### `core.echo`
Outputs a message to stdout.
**Parameters:**
- `message` (string, required): Message to echo
- `uppercase` (boolean, optional): Convert message to uppercase
**Example:**
```yaml
action: core.echo
parameters:
message: "Hello, Attune!"
uppercase: false
```
---
#### `core.sleep`
Pauses execution for a specified duration.
**Parameters:**
- `seconds` (integer, required): Number of seconds to sleep (0-3600)
- `message` (string, optional): Optional message to display before sleeping
**Example:**
```yaml
action: core.sleep
parameters:
seconds: 30
message: "Waiting 30 seconds..."
```
---
#### `core.noop`
Does nothing - useful for testing and placeholder workflows.
**Parameters:**
- `message` (string, optional): Optional message to log
- `exit_code` (integer, optional): Exit code to return (default: 0)
**Example:**
```yaml
action: core.noop
parameters:
message: "Testing workflow structure"
```
---
#### `core.http_request`
Make HTTP requests to external APIs with full control over headers, authentication, and request body.
**Parameters:**
- `url` (string, required): URL to send the request to
- `method` (string, optional): HTTP method (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS)
- `headers` (object, optional): HTTP headers as key-value pairs
- `body` (string, optional): Request body for POST/PUT/PATCH
- `json_body` (object, optional): JSON request body (alternative to `body`)
- `query_params` (object, optional): URL query parameters
- `timeout` (integer, optional): Request timeout in seconds (default: 30)
- `verify_ssl` (boolean, optional): Verify SSL certificates (default: true)
- `auth_type` (string, optional): Authentication type (none, basic, bearer)
- `auth_username` (string, optional): Username for basic auth
- `auth_password` (string, secret, optional): Password for basic auth
- `auth_token` (string, secret, optional): Bearer token
- `follow_redirects` (boolean, optional): Follow HTTP redirects (default: true)
- `max_redirects` (integer, optional): Maximum redirects to follow (default: 10)
**Output:**
- `status_code` (integer): HTTP status code
- `headers` (object): Response headers
- `body` (string): Response body as text
- `json` (object): Parsed JSON response (if applicable)
- `elapsed_ms` (integer): Request duration in milliseconds
- `url` (string): Final URL after redirects
- `success` (boolean): Whether request was successful (2xx status)
**Example:**
```yaml
action: core.http_request
parameters:
url: "https://api.example.com/users"
method: "POST"
json_body:
name: "John Doe"
email: "john@example.com"
headers:
Content-Type: "application/json"
auth_type: "bearer"
auth_token: "${secret:api_token}"
```
---
### Triggers
#### `core.intervaltimer`
Fires at regular intervals based on time unit and interval.
**Parameters:**
- `unit` (string, required): Time unit (seconds, minutes, hours)
- `interval` (integer, required): Number of time units between triggers
**Payload:**
- `type`: "interval"
- `interval_seconds`: Total interval in seconds
- `fired_at`: ISO 8601 timestamp
- `execution_count`: Number of times fired
- `sensor_ref`: Reference to the sensor
**Example:**
```yaml
trigger: core.intervaltimer
config:
unit: "minutes"
interval: 5
```
---
#### `core.crontimer`
Fires based on cron schedule expressions.
**Parameters:**
- `expression` (string, required): Cron expression (6 fields: second minute hour day month weekday)
- `timezone` (string, optional): Timezone (default: UTC)
- `description` (string, optional): Human-readable schedule description
**Payload:**
- `type`: "cron"
- `fired_at`: ISO 8601 timestamp
- `scheduled_at`: When trigger was scheduled to fire
- `expression`: The cron expression
- `timezone`: Timezone used
- `next_fire_at`: Next scheduled fire time
- `execution_count`: Number of times fired
- `sensor_ref`: Reference to the sensor
**Cron Format:**
```
┌───────── second (0-59)
│ ┌─────── minute (0-59)
│ │ ┌───── hour (0-23)
│ │ │ ┌─── day of month (1-31)
│ │ │ │ ┌─ month (1-12)
│ │ │ │ │ ┌ day of week (0-6, 0=Sunday)
│ │ │ │ │ │
* * * * * *
```
**Examples:**
- `0 0 * * * *` - Every hour
- `0 0 0 * * *` - Every day at midnight
- `0 */15 * * * *` - Every 15 minutes
- `0 30 8 * * 1-5` - 8:30 AM on weekdays
---
#### `core.datetimetimer`
Fires once at a specific date and time.
**Parameters:**
- `fire_at` (string, required): ISO 8601 timestamp when timer should fire
- `timezone` (string, optional): Timezone (default: UTC)
- `description` (string, optional): Human-readable description
**Payload:**
- `type`: "one_shot"
- `fire_at`: Scheduled fire time
- `fired_at`: Actual fire time
- `timezone`: Timezone used
- `delay_ms`: Delay between scheduled and actual fire time
- `sensor_ref`: Reference to the sensor
**Example:**
```yaml
trigger: core.datetimetimer
config:
fire_at: "2024-12-31T23:59:59Z"
description: "New Year's countdown"
```
---
### Sensors
#### `core.interval_timer_sensor`
Built-in sensor that monitors time and fires interval timer triggers.
**Configuration:**
- `check_interval_seconds` (integer, optional): How often to check triggers (default: 1)
This sensor automatically runs as part of the Attune sensor service and manages all interval timer trigger instances.
---
## Configuration
The core pack supports the following configuration options:
```yaml
# config.yaml
packs:
core:
max_action_timeout: 300 # Maximum action timeout in seconds
enable_debug_logging: false # Enable debug logging
```
## Dependencies
### Python Dependencies
- `requests>=2.28.0` - For HTTP request action
- `croniter>=1.4.0` - For cron timer parsing (future)
### Runtime Dependencies
- Shell (bash/sh) - For shell-based actions
- Python 3.8+ - For Python-based actions and sensors
## Installation
The core pack is automatically installed with Attune. No manual installation is required.
To verify the core pack is loaded:
```bash
# Using CLI
attune pack list | grep core
# Using API
curl http://localhost:8080/api/v1/packs/core
```
## Usage Examples
### Example 1: Echo Every 10 Seconds
Create a rule that echoes "Hello, World!" every 10 seconds:
```yaml
ref: core.hello_world_rule
trigger: core.intervaltimer
trigger_config:
unit: "seconds"
interval: 10
action: core.echo
action_params:
message: "Hello, World!"
uppercase: false
```
### Example 2: HTTP Health Check Every 5 Minutes
Monitor an API endpoint every 5 minutes:
```yaml
ref: core.health_check_rule
trigger: core.intervaltimer
trigger_config:
unit: "minutes"
interval: 5
action: core.http_request
action_params:
url: "https://api.example.com/health"
method: "GET"
timeout: 10
```
### Example 3: Daily Report at Midnight
Generate a report every day at midnight:
```yaml
ref: core.daily_report_rule
trigger: core.crontimer
trigger_config:
expression: "0 0 0 * * *"
timezone: "UTC"
description: "Daily at midnight"
action: core.http_request
action_params:
url: "https://api.example.com/reports/generate"
method: "POST"
```
### Example 4: One-Time Reminder
Set a one-time reminder for a specific date and time:
```yaml
ref: core.meeting_reminder
trigger: core.datetimetimer
trigger_config:
fire_at: "2024-06-15T14:00:00Z"
description: "Team meeting reminder"
action: core.echo
action_params:
message: "Team meeting starts in 15 minutes!"
```
## Development
### Adding New Actions
1. Create action metadata file: `actions/<action_name>.yaml`
2. Create action implementation: `actions/<action_name>.sh` or `actions/<action_name>.py`
3. Make script executable: `chmod +x actions/<action_name>.sh`
4. Update pack manifest if needed
5. Test the action
### Testing Actions Locally
Test actions directly by setting environment variables:
```bash
# Test echo action
export ATTUNE_ACTION_MESSAGE="Test message"
export ATTUNE_ACTION_UPPERCASE=true
./actions/echo.sh
# Test HTTP request action
export ATTUNE_ACTION_URL="https://httpbin.org/get"
export ATTUNE_ACTION_METHOD="GET"
python3 actions/http_request.py
```
## Contributing
The core pack is part of the Attune project. Contributions are welcome!
1. Follow the existing code style and structure
2. Add tests for new actions/sensors
3. Update documentation
4. Submit a pull request
## License
The core pack is licensed under the same license as Attune.
## Support
- Documentation: https://docs.attune.io/packs/core
- Issues: https://github.com/attune-io/attune/issues
- Discussions: https://github.com/attune-io/attune/discussions

305
packs/core/SETUP.md Normal file
View File

@@ -0,0 +1,305 @@
# Core Pack Setup Guide
This guide explains how to set up and load the Attune core pack into your database.
## Overview
The **core pack** is Attune's built-in system pack that provides essential automation components including:
- **Timer Triggers**: Interval-based, cron-based, and datetime triggers
- **Basic Actions**: Echo, sleep, noop, and HTTP request actions
- **Built-in Sensors**: Interval timer sensor for time-based automation
The core pack must be loaded into the database before it can be used in rules and workflows.
## Prerequisites
Before loading the core pack, ensure:
1. **PostgreSQL is running** and accessible
2. **Database migrations are applied**: `sqlx migrate run`
3. **Python 3.8+** is installed (for the loader script)
4. **Required Python packages** are installed:
```bash
pip install psycopg2-binary pyyaml
```
## Loading Methods
### Method 1: Python Loader Script (Recommended)
The Python loader script reads the pack YAML files and creates database entries automatically.
**Usage:**
```bash
# From the project root
python3 scripts/load_core_pack.py
# With custom database URL
python3 scripts/load_core_pack.py --database-url "postgresql://user:pass@localhost:5432/attune"
# With custom pack directory
python3 scripts/load_core_pack.py --pack-dir ./packs
```
**What it does:**
- Reads `pack.yaml` for pack metadata
- Loads all trigger definitions from `triggers/*.yaml`
- Loads all action definitions from `actions/*.yaml`
- Loads all sensor definitions from `sensors/*.yaml`
- Creates or updates database entries (idempotent)
- Uses transactions (all-or-nothing)
**Output:**
```
============================================================
Core Pack Loader
============================================================
→ Loading pack metadata...
✓ Pack 'core' loaded (ID: 1)
→ Loading triggers...
✓ Trigger 'core.intervaltimer' (ID: 1)
✓ Trigger 'core.crontimer' (ID: 2)
✓ Trigger 'core.datetimetimer' (ID: 3)
→ Loading actions...
✓ Action 'core.echo' (ID: 1)
✓ Action 'core.sleep' (ID: 2)
✓ Action 'core.noop' (ID: 3)
✓ Action 'core.http_request' (ID: 4)
→ Loading sensors...
✓ Sensor 'core.interval_timer_sensor' (ID: 1)
============================================================
✓ Core pack loaded successfully!
============================================================
Pack ID: 1
Triggers: 3
Actions: 4
Sensors: 1
```
### Method 2: SQL Seed Script
For simpler setups or CI/CD, you can use the SQL seed script directly.
**Usage:**
```bash
psql $DATABASE_URL -f scripts/seed_core_pack.sql
```
**Note:** The SQL script may not include all pack metadata and is less flexible than the Python loader.
### Method 3: CLI (Future)
Once the CLI pack management commands are fully implemented:
```bash
attune pack register ./packs/core
```
## Verification
After loading, verify the core pack is available:
### Using CLI
```bash
# List all packs
attune pack list
# Show core pack details
attune pack show core
# List core pack actions
attune action list --pack core
# List core pack triggers
attune trigger list --pack core
```
### Using API
```bash
# Get pack info
curl http://localhost:8080/api/v1/packs/core | jq
# List actions
curl http://localhost:8080/api/v1/packs/core/actions | jq
# List triggers
curl http://localhost:8080/api/v1/packs/core/triggers | jq
```
### Using Database
```sql
-- Check pack exists
SELECT * FROM attune.pack WHERE ref = 'core';
-- Count components
SELECT
(SELECT COUNT(*) FROM attune.trigger WHERE pack_ref = 'core') as triggers,
(SELECT COUNT(*) FROM attune.action WHERE pack_ref = 'core') as actions,
(SELECT COUNT(*) FROM attune.sensor WHERE pack_ref = 'core') as sensors;
```
## Testing the Core Pack
### 1. Test Actions Directly
Test actions using environment variables:
```bash
# Test echo action
export ATTUNE_ACTION_MESSAGE="Hello, Attune!"
export ATTUNE_ACTION_UPPERCASE=false
./packs/core/actions/echo.sh
# Test sleep action
export ATTUNE_ACTION_SECONDS=2
export ATTUNE_ACTION_MESSAGE="Sleeping..."
./packs/core/actions/sleep.sh
# Test HTTP request action
export ATTUNE_ACTION_URL="https://httpbin.org/get"
export ATTUNE_ACTION_METHOD="GET"
python3 packs/core/actions/http_request.py
```
### 2. Run Pack Test Suite
```bash
# Run comprehensive test suite
./packs/core/test_core_pack.sh
```
### 3. Create a Test Rule
Create a simple rule to test the core pack integration:
```bash
# Create a rule that echoes every 10 seconds
attune rule create \
--name "test_timer_echo" \
--trigger "core.intervaltimer" \
--trigger-config '{"unit":"seconds","interval":10}' \
--action "core.echo" \
--action-params '{"message":"Timer triggered!"}' \
--enabled
```
## Updating the Core Pack
To update the core pack after making changes:
1. Edit the relevant YAML files in `packs/core/`
2. Re-run the loader script:
```bash
python3 scripts/load_core_pack.py
```
3. The loader will update existing entries (upsert)
## Troubleshooting
### "Failed to connect to database"
- Verify PostgreSQL is running: `pg_isready`
- Check `DATABASE_URL` environment variable
- Test connection: `psql $DATABASE_URL -c "SELECT 1"`
### "pack.yaml not found"
- Ensure you're running from the project root
- Check the `--pack-dir` argument points to the correct directory
- Verify `packs/core/pack.yaml` exists
### "ModuleNotFoundError: No module named 'psycopg2'"
```bash
pip install psycopg2-binary pyyaml
```
### "Pack loaded but not visible in API"
- Restart the API service to reload pack data
- Check pack is enabled: `SELECT enabled FROM attune.pack WHERE ref = 'core'`
### Actions not executing
- Verify action scripts are executable: `chmod +x packs/core/actions/*.sh`
- Check worker service is running and can access the packs directory
- Verify runtime configuration is correct
## Development Workflow
When developing new core pack components:
1. **Add new action:**
- Create `actions/new_action.yaml` with metadata
- Create `actions/new_action.sh` (or `.py`) with implementation
- Make script executable: `chmod +x actions/new_action.sh`
- Test locally: `export ATTUNE_ACTION_*=... && ./actions/new_action.sh`
- Load into database: `python3 scripts/load_core_pack.py`
2. **Add new trigger:**
- Create `triggers/new_trigger.yaml` with metadata
- Load into database: `python3 scripts/load_core_pack.py`
- Create sensor if needed
3. **Add new sensor:**
- Create `sensors/new_sensor.yaml` with metadata
- Create `sensors/new_sensor.py` with implementation
- Load into database: `python3 scripts/load_core_pack.py`
- Restart sensor service
## Environment Variables
The loader script supports the following environment variables:
- `DATABASE_URL` - PostgreSQL connection string
- Default: `postgresql://postgres:postgres@localhost:5432/attune`
- Example: `postgresql://user:pass@db.example.com:5432/attune`
- `ATTUNE_PACKS_DIR` - Base directory for packs
- Default: `./packs`
- Example: `/opt/attune/packs`
## CI/CD Integration
For automated deployments:
```yaml
# Example GitHub Actions workflow
- name: Load Core Pack
run: |
python3 scripts/load_core_pack.py \
--database-url "${{ secrets.DATABASE_URL }}"
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
```
## Next Steps
After loading the core pack:
1. **Create your first rule** using core triggers and actions
2. **Enable sensors** to start generating events
3. **Monitor executions** via the API or Web UI
4. **Explore pack documentation** in `README.md`
## Additional Resources
- **Pack README**: `packs/core/README.md` - Comprehensive component documentation
- **Testing Guide**: `packs/core/TESTING.md` - Testing procedures
- **API Documentation**: `docs/api-packs.md` - Pack management API
- **Action Development**: `docs/action-development.md` - Creating custom actions
## Support
If you encounter issues:
1. Check this troubleshooting section
2. Review logs from services (api, executor, worker, sensor)
3. Verify database state with SQL queries
4. File an issue with detailed error messages and logs
---
**Last Updated:** 2025-01-20
**Core Pack Version:** 1.0.0

410
packs/core/TESTING.md Normal file
View File

@@ -0,0 +1,410 @@
# Core Pack Testing Guide
Quick reference for testing core pack actions and sensors locally.
---
## Prerequisites
```bash
# Ensure scripts are executable
chmod +x packs/core/actions/*.sh
chmod +x packs/core/actions/*.py
chmod +x packs/core/sensors/*.py
# Install Python dependencies
pip install requests>=2.28.0
```
---
## Testing Actions
Actions receive parameters via environment variables prefixed with `ATTUNE_ACTION_`.
### Test `core.echo`
```bash
# Basic echo
export ATTUNE_ACTION_MESSAGE="Hello, Attune!"
./packs/core/actions/echo.sh
# With uppercase conversion
export ATTUNE_ACTION_MESSAGE="test message"
export ATTUNE_ACTION_UPPERCASE=true
./packs/core/actions/echo.sh
```
**Expected Output:**
```
Hello, Attune!
TEST MESSAGE
```
---
### Test `core.sleep`
```bash
# Sleep for 2 seconds
export ATTUNE_ACTION_SECONDS=2
export ATTUNE_ACTION_MESSAGE="Sleeping..."
time ./packs/core/actions/sleep.sh
```
**Expected Output:**
```
Sleeping...
Slept for 2 seconds
real 0m2.004s
```
---
### Test `core.noop`
```bash
# No operation with message
export ATTUNE_ACTION_MESSAGE="Testing noop"
./packs/core/actions/noop.sh
# With custom exit code
export ATTUNE_ACTION_EXIT_CODE=0
./packs/core/actions/noop.sh
echo "Exit code: $?"
```
**Expected Output:**
```
[NOOP] Testing noop
No operation completed successfully
Exit code: 0
```
---
### Test `core.http_request`
```bash
# Simple GET request
export ATTUNE_ACTION_URL="https://httpbin.org/get"
export ATTUNE_ACTION_METHOD="GET"
python3 ./packs/core/actions/http_request.py
# POST with JSON body
export ATTUNE_ACTION_URL="https://httpbin.org/post"
export ATTUNE_ACTION_METHOD="POST"
export ATTUNE_ACTION_JSON_BODY='{"name": "test", "value": 123}'
python3 ./packs/core/actions/http_request.py
# With custom headers
export ATTUNE_ACTION_URL="https://httpbin.org/headers"
export ATTUNE_ACTION_METHOD="GET"
export ATTUNE_ACTION_HEADERS='{"X-Custom-Header": "test-value"}'
python3 ./packs/core/actions/http_request.py
# With query parameters
export ATTUNE_ACTION_URL="https://httpbin.org/get"
export ATTUNE_ACTION_METHOD="GET"
export ATTUNE_ACTION_QUERY_PARAMS='{"foo": "bar", "page": "1"}'
python3 ./packs/core/actions/http_request.py
# With timeout
export ATTUNE_ACTION_URL="https://httpbin.org/delay/5"
export ATTUNE_ACTION_METHOD="GET"
export ATTUNE_ACTION_TIMEOUT=2
python3 ./packs/core/actions/http_request.py
```
**Expected Output:**
```json
{
"status_code": 200,
"headers": {
"Content-Type": "application/json",
...
},
"body": "...",
"json": {
"args": {},
"headers": {...},
...
},
"elapsed_ms": 234,
"url": "https://httpbin.org/get",
"success": true
}
```
---
## Testing Sensors
Sensors receive configuration via environment variables prefixed with `ATTUNE_SENSOR_`.
### Test `core.interval_timer_sensor`
```bash
# Create test trigger instances JSON
export ATTUNE_SENSOR_TRIGGERS='[
{
"id": 1,
"ref": "core.intervaltimer",
"config": {
"unit": "seconds",
"interval": 5
}
}
]'
# Run sensor (will output events every 5 seconds)
python3 ./packs/core/sensors/interval_timer_sensor.py
```
**Expected Output:**
```
Interval Timer Sensor started (check_interval=1s)
{"type": "interval", "interval_seconds": 5, "fired_at": "2024-01-20T12:00:00Z", "execution_count": 1, "sensor_ref": "core.interval_timer_sensor", "trigger_instance_id": 1, "trigger_ref": "core.intervaltimer"}
{"type": "interval", "interval_seconds": 5, "fired_at": "2024-01-20T12:00:05Z", "execution_count": 2, "sensor_ref": "core.interval_timer_sensor", "trigger_instance_id": 1, "trigger_ref": "core.intervaltimer"}
...
```
Press `Ctrl+C` to stop the sensor.
---
## Testing with Multiple Trigger Instances
```bash
# Test multiple timers
export ATTUNE_SENSOR_TRIGGERS='[
{
"id": 1,
"ref": "core.intervaltimer",
"config": {"unit": "seconds", "interval": 3}
},
{
"id": 2,
"ref": "core.intervaltimer",
"config": {"unit": "seconds", "interval": 5}
},
{
"id": 3,
"ref": "core.intervaltimer",
"config": {"unit": "seconds", "interval": 10}
}
]'
python3 ./packs/core/sensors/interval_timer_sensor.py
```
You should see events firing at different intervals (3s, 5s, 10s).
---
## Validation Tests
### Validate YAML Schemas
```bash
# Install yamllint (optional)
pip install yamllint
# Validate all YAML files
yamllint packs/core/**/*.yaml
```
### Validate JSON Schemas
```bash
# Check parameter schemas are valid JSON Schema
cat packs/core/actions/http_request.yaml | grep -A 50 "parameters:" | python3 -c "
import sys, yaml, json
data = yaml.safe_load(sys.stdin)
print(json.dumps(data, indent=2))
"
```
---
## Error Testing
### Test Invalid Parameters
```bash
# Invalid seconds value for sleep
export ATTUNE_ACTION_SECONDS=-1
./packs/core/actions/sleep.sh
# Expected: ERROR: seconds must be between 0 and 3600
# Invalid exit code for noop
export ATTUNE_ACTION_EXIT_CODE=999
./packs/core/actions/noop.sh
# Expected: ERROR: exit_code must be between 0 and 255
# Missing required parameter for HTTP request
unset ATTUNE_ACTION_URL
python3 ./packs/core/actions/http_request.py
# Expected: ERROR: Required parameter 'url' not provided
```
---
## Performance Testing
### Measure Action Execution Time
```bash
# Echo action
time for i in {1..100}; do
export ATTUNE_ACTION_MESSAGE="Test $i"
./packs/core/actions/echo.sh > /dev/null
done
# HTTP request action
time for i in {1..10}; do
export ATTUNE_ACTION_URL="https://httpbin.org/get"
python3 ./packs/core/actions/http_request.py > /dev/null
done
```
---
## Integration Testing (with Attune Services)
### Prerequisites
```bash
# Start Attune services
docker-compose up -d postgres rabbitmq redis
# Run migrations
sqlx migrate run
# Load core pack (future)
# attune pack load packs/core
```
### Test Action Execution via API
```bash
# Create execution manually
curl -X POST http://localhost:8080/api/v1/executions \
-H "Content-Type: application/json" \
-d '{
"action_ref": "core.echo",
"parameters": {
"message": "API test",
"uppercase": true
}
}'
# Check execution status
curl http://localhost:8080/api/v1/executions/{execution_id}
```
### Test Sensor via Sensor Service
```bash
# Start sensor service (future)
# cargo run --bin attune-sensor
# Check events created
curl http://localhost:8080/api/v1/events?limit=10
```
---
## Troubleshooting
### Action Not Executing
```bash
# Check file permissions
ls -la packs/core/actions/
# Ensure scripts are executable
chmod +x packs/core/actions/*.sh
chmod +x packs/core/actions/*.py
```
### Python Import Errors
```bash
# Install required packages
pip install requests>=2.28.0
# Verify Python version
python3 --version # Should be 3.8+
```
### Environment Variables Not Working
```bash
# Print all ATTUNE_* environment variables
env | grep ATTUNE_
# Test with explicit export
export ATTUNE_ACTION_MESSAGE="test"
echo $ATTUNE_ACTION_MESSAGE
```
---
## Automated Test Script
Create a test script `test_core_pack.sh`:
```bash
#!/bin/bash
set -e
echo "Testing Core Pack Actions..."
# Test echo
echo "→ Testing core.echo..."
export ATTUNE_ACTION_MESSAGE="Test"
./packs/core/actions/echo.sh > /dev/null
echo "✓ core.echo passed"
# Test sleep
echo "→ Testing core.sleep..."
export ATTUNE_ACTION_SECONDS=1
./packs/core/actions/sleep.sh > /dev/null
echo "✓ core.sleep passed"
# Test noop
echo "→ Testing core.noop..."
export ATTUNE_ACTION_MESSAGE="test"
./packs/core/actions/noop.sh > /dev/null
echo "✓ core.noop passed"
# Test HTTP request
echo "→ Testing core.http_request..."
export ATTUNE_ACTION_URL="https://httpbin.org/get"
export ATTUNE_ACTION_METHOD="GET"
python3 ./packs/core/actions/http_request.py > /dev/null
echo "✓ core.http_request passed"
echo ""
echo "All tests passed! ✓"
```
Run with:
```bash
chmod +x test_core_pack.sh
./test_core_pack.sh
```
---
## Next Steps
1. Implement pack loader to register components in database
2. Update worker service to execute actions from filesystem
3. Update sensor service to run sensors from filesystem
4. Add comprehensive integration tests
5. Create CLI commands for pack management
See `docs/core-pack-integration.md` for implementation details.

21
packs/core/actions/echo.sh Executable file
View File

@@ -0,0 +1,21 @@
#!/bin/bash
# Echo Action - Core Pack
# Outputs a message to stdout with optional uppercase conversion
set -e
# Parse parameters from environment variables
# Attune passes action parameters as environment variables prefixed with ATTUNE_ACTION_
MESSAGE="${ATTUNE_ACTION_MESSAGE:-Hello, World!}"
UPPERCASE="${ATTUNE_ACTION_UPPERCASE:-false}"
# Convert to uppercase if requested
if [ "$UPPERCASE" = "true" ]; then
MESSAGE=$(echo "$MESSAGE" | tr '[:lower:]' '[:upper:]')
fi
# Echo the message
echo "$MESSAGE"
# Exit successfully
exit 0

View File

@@ -0,0 +1,51 @@
# Echo Action
# Outputs a message to stdout
name: echo
ref: core.echo
description: "Echo a message to stdout"
enabled: true
# Runner type determines how the action is executed
runner_type: shell
# Entry point is the shell command or script to execute
entry_point: echo.sh
# Action parameters schema (standard JSON Schema format)
parameters:
type: object
properties:
message:
type: string
description: "Message to echo"
default: "Hello, World!"
uppercase:
type: boolean
description: "Convert message to uppercase before echoing"
default: false
required:
- message
# Output schema
output_schema:
type: object
properties:
stdout:
type: string
description: "Standard output from the echo command"
stderr:
type: string
description: "Standard error output (usually empty)"
exit_code:
type: integer
description: "Exit code of the command (0 = success)"
result:
type: string
description: "The echoed message"
# Tags for categorization
tags:
- utility
- testing
- debug

View File

@@ -0,0 +1,206 @@
#!/usr/bin/env python3
"""
HTTP Request Action - Core Pack
Make HTTP requests to external APIs with support for various methods, headers, and authentication
"""
import json
import os
import sys
import time
from typing import Any, Dict, Optional
try:
import requests
from requests.auth import HTTPBasicAuth
except ImportError:
print(
"ERROR: requests library not installed. Run: pip install requests>=2.28.0",
file=sys.stderr,
)
sys.exit(1)
def get_env_param(name: str, default: Any = None, required: bool = False) -> Any:
"""Get action parameter from environment variable."""
env_key = f"ATTUNE_ACTION_{name.upper()}"
value = os.environ.get(env_key, default)
if required and value is None:
raise ValueError(f"Required parameter '{name}' not provided")
return value
def parse_json_param(name: str, default: Any = None) -> Any:
"""Parse JSON parameter from environment variable."""
value = get_env_param(name)
if value is None:
return default
try:
return json.loads(value)
except json.JSONDecodeError as e:
raise ValueError(f"Invalid JSON for parameter '{name}': {e}")
def parse_bool_param(name: str, default: bool = False) -> bool:
"""Parse boolean parameter from environment variable."""
value = get_env_param(name)
if value is None:
return default
if isinstance(value, bool):
return value
return str(value).lower() in ("true", "1", "yes", "on")
def parse_int_param(name: str, default: int = 0) -> int:
"""Parse integer parameter from environment variable."""
value = get_env_param(name)
if value is None:
return default
try:
return int(value)
except (ValueError, TypeError):
raise ValueError(f"Invalid integer for parameter '{name}': {value}")
def make_http_request() -> Dict[str, Any]:
"""Execute HTTP request with provided parameters."""
# Parse required parameters
url = get_env_param("url", required=True)
# Parse optional parameters
method = get_env_param("method", "GET").upper()
headers = parse_json_param("headers", {})
body = get_env_param("body")
json_body = parse_json_param("json_body")
query_params = parse_json_param("query_params", {})
timeout = parse_int_param("timeout", 30)
verify_ssl = parse_bool_param("verify_ssl", True)
auth_type = get_env_param("auth_type", "none")
follow_redirects = parse_bool_param("follow_redirects", True)
max_redirects = parse_int_param("max_redirects", 10)
# Prepare request kwargs
request_kwargs = {
"method": method,
"url": url,
"headers": headers,
"params": query_params,
"timeout": timeout,
"verify": verify_ssl,
"allow_redirects": follow_redirects,
}
# Handle authentication
if auth_type == "basic":
username = get_env_param("auth_username")
password = get_env_param("auth_password")
if username and password:
request_kwargs["auth"] = HTTPBasicAuth(username, password)
elif auth_type == "bearer":
token = get_env_param("auth_token")
if token:
request_kwargs["headers"]["Authorization"] = f"Bearer {token}"
# Handle request body
if json_body is not None:
request_kwargs["json"] = json_body
elif body is not None:
request_kwargs["data"] = body
# Make the request
start_time = time.time()
try:
response = requests.request(**request_kwargs)
elapsed_ms = int((time.time() - start_time) * 1000)
# Parse response
result = {
"status_code": response.status_code,
"headers": dict(response.headers),
"body": response.text,
"elapsed_ms": elapsed_ms,
"url": response.url,
"success": 200 <= response.status_code < 300,
}
# Try to parse JSON response
try:
result["json"] = response.json()
except (json.JSONDecodeError, ValueError):
result["json"] = None
return result
except requests.exceptions.Timeout:
return {
"status_code": 0,
"headers": {},
"body": "",
"json": None,
"elapsed_ms": int((time.time() - start_time) * 1000),
"url": url,
"success": False,
"error": "Request timeout",
}
except requests.exceptions.ConnectionError as e:
return {
"status_code": 0,
"headers": {},
"body": "",
"json": None,
"elapsed_ms": int((time.time() - start_time) * 1000),
"url": url,
"success": False,
"error": f"Connection error: {str(e)}",
}
except requests.exceptions.RequestException as e:
return {
"status_code": 0,
"headers": {},
"body": "",
"json": None,
"elapsed_ms": int((time.time() - start_time) * 1000),
"url": url,
"success": False,
"error": f"Request error: {str(e)}",
}
def main():
"""Main entry point for the action."""
try:
result = make_http_request()
# Output result as JSON
print(json.dumps(result, indent=2))
# Exit with success/failure based on HTTP status
if result.get("success", False):
sys.exit(0)
else:
# Non-2xx status code or error
error = result.get("error")
if error:
print(f"ERROR: {error}", file=sys.stderr)
else:
print(
f"ERROR: HTTP request failed with status {result.get('status_code')}",
file=sys.stderr,
)
sys.exit(1)
except Exception as e:
print(f"ERROR: {str(e)}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,119 @@
# HTTP Request Action
# Make HTTP requests to external APIs
name: http_request
ref: core.http_request
description: "Make HTTP requests to external APIs with support for various methods, headers, and authentication"
enabled: true
# Runner type determines how the action is executed
runner_type: python
# Entry point is the Python script to execute
entry_point: http_request.py
# Action parameters schema (standard JSON Schema format)
parameters:
type: object
properties:
url:
type: string
description: "URL to send the request to"
method:
type: string
description: "HTTP method to use"
default: "GET"
enum:
- GET
- POST
- PUT
- PATCH
- DELETE
- HEAD
- OPTIONS
headers:
type: object
description: "HTTP headers to include in the request"
default: {}
body:
type: string
description: "Request body (for POST, PUT, PATCH methods)"
json_body:
type: object
description: "JSON request body (alternative to body parameter)"
query_params:
type: object
description: "URL query parameters as key-value pairs"
default: {}
timeout:
type: integer
description: "Request timeout in seconds"
default: 30
minimum: 1
maximum: 300
verify_ssl:
type: boolean
description: "Verify SSL certificates"
default: true
auth_type:
type: string
description: "Authentication type"
enum:
- none
- basic
- bearer
auth_username:
type: string
description: "Username for basic authentication"
auth_password:
type: string
description: "Password for basic authentication"
secret: true
auth_token:
type: string
description: "Bearer token for bearer authentication"
secret: true
follow_redirects:
type: boolean
description: "Follow HTTP redirects"
default: true
max_redirects:
type: integer
description: "Maximum number of redirects to follow"
default: 10
required:
- url
# Output schema
output_schema:
type: object
properties:
status_code:
type: integer
description: "HTTP status code"
headers:
type: object
description: "Response headers"
body:
type: string
description: "Response body as text"
json:
type: object
description: "Parsed JSON response (if applicable)"
elapsed_ms:
type: integer
description: "Request duration in milliseconds"
url:
type: string
description: "Final URL after redirects"
success:
type: boolean
description: "Whether the request was successful (2xx status code)"
# Tags for categorization
tags:
- http
- api
- web
- utility
- integration

31
packs/core/actions/noop.sh Executable file
View File

@@ -0,0 +1,31 @@
#!/bin/bash
# No Operation Action - Core Pack
# Does nothing - useful for testing and placeholder workflows
set -e
# Parse parameters from environment variables
MESSAGE="${ATTUNE_ACTION_MESSAGE:-}"
EXIT_CODE="${ATTUNE_ACTION_EXIT_CODE:-0}"
# Validate exit code parameter
if ! [[ "$EXIT_CODE" =~ ^[0-9]+$ ]]; then
echo "ERROR: exit_code must be a positive integer" >&2
exit 1
fi
if [ "$EXIT_CODE" -lt 0 ] || [ "$EXIT_CODE" -gt 255 ]; then
echo "ERROR: exit_code must be between 0 and 255" >&2
exit 1
fi
# Log message if provided
if [ -n "$MESSAGE" ]; then
echo "[NOOP] $MESSAGE"
fi
# Output result
echo "No operation completed successfully"
# Exit with specified code
exit "$EXIT_CODE"

View File

@@ -0,0 +1,52 @@
# No Operation Action
# Does nothing - useful for testing and placeholder workflows
name: noop
ref: core.noop
description: "Does nothing - useful for testing and placeholder workflows"
enabled: true
# Runner type determines how the action is executed
runner_type: shell
# Entry point is the shell command or script to execute
entry_point: noop.sh
# Action parameters schema (standard JSON Schema format)
parameters:
type: object
properties:
message:
type: string
description: "Optional message to log (for debugging)"
exit_code:
type: integer
description: "Exit code to return (default: 0 for success)"
default: 0
minimum: 0
maximum: 255
required: []
# Output schema
output_schema:
type: object
properties:
stdout:
type: string
description: "Standard output (empty unless message provided)"
stderr:
type: string
description: "Standard error output (usually empty)"
exit_code:
type: integer
description: "Exit code of the command"
result:
type: string
description: "Operation result"
# Tags for categorization
tags:
- utility
- testing
- placeholder
- noop

34
packs/core/actions/sleep.sh Executable file
View File

@@ -0,0 +1,34 @@
#!/bin/bash
# Sleep Action - Core Pack
# Pauses execution for a specified duration
set -e
# Parse parameters from environment variables
SLEEP_SECONDS="${ATTUNE_ACTION_SECONDS:-1}"
MESSAGE="${ATTUNE_ACTION_MESSAGE:-}"
# Validate seconds parameter
if ! [[ "$SLEEP_SECONDS" =~ ^[0-9]+$ ]]; then
echo "ERROR: seconds must be a positive integer" >&2
exit 1
fi
if [ "$SLEEP_SECONDS" -lt 0 ] || [ "$SLEEP_SECONDS" -gt 3600 ]; then
echo "ERROR: seconds must be between 0 and 3600" >&2
exit 1
fi
# Display message if provided
if [ -n "$MESSAGE" ]; then
echo "$MESSAGE"
fi
# Sleep for the specified duration
sleep "$SLEEP_SECONDS"
# Output result
echo "Slept for $SLEEP_SECONDS seconds"
# Exit successfully
exit 0

View File

@@ -0,0 +1,53 @@
# Sleep Action
# Pauses execution for a specified duration
name: sleep
ref: core.sleep
description: "Sleep for a specified number of seconds"
enabled: true
# Runner type determines how the action is executed
runner_type: shell
# Entry point is the shell command or script to execute
entry_point: sleep.sh
# Action parameters schema (standard JSON Schema format)
parameters:
type: object
properties:
seconds:
type: integer
description: "Number of seconds to sleep"
default: 1
minimum: 0
maximum: 3600
message:
type: string
description: "Optional message to display before sleeping"
required:
- seconds
# Output schema
output_schema:
type: object
properties:
stdout:
type: string
description: "Standard output (empty unless message provided)"
stderr:
type: string
description: "Standard error output (usually empty)"
exit_code:
type: integer
description: "Exit code of the command (0 = success)"
duration:
type: integer
description: "Number of seconds slept"
# Tags for categorization
tags:
- utility
- testing
- delay
- timing

100
packs/core/pack.yaml Normal file
View File

@@ -0,0 +1,100 @@
# Attune Core Pack
# Built-in core functionality including timers, utilities, and basic actions
ref: core
label: "Core Pack"
description: "Built-in core functionality including timer triggers, HTTP utilities, and basic shell actions"
version: "1.0.0"
author: "Attune Team"
email: "core@attune.io"
# Pack is a system pack (shipped with Attune)
system: true
# Pack configuration schema (minimal for core pack)
conf_schema:
type: object
properties:
max_action_timeout:
type: integer
description: "Maximum timeout for action execution in seconds"
default: 300
minimum: 1
maximum: 3600
enable_debug_logging:
type: boolean
description: "Enable debug logging for core pack actions"
default: false
required: []
# Default pack configuration
config:
max_action_timeout: 300
enable_debug_logging: false
# Pack metadata
meta:
category: "system"
keywords:
- "core"
- "utilities"
- "timers"
- "http"
- "shell"
# Python dependencies for Python-based actions
python_dependencies:
- "requests>=2.28.0"
- "croniter>=1.4.0"
# Documentation
documentation_url: "https://docs.attune.io/packs/core"
repository_url: "https://github.com/attune-io/attune"
# Pack tags for discovery
tags:
- core
- system
- utilities
- timers
# Runtime dependencies
runtime_deps:
- shell
- python3
# Enabled by default
enabled: true
# Pack Testing Configuration
testing:
# Enable testing during installation
enabled: true
# Test discovery method
discovery:
method: "directory"
path: "tests"
# Test runners by runtime type
runners:
shell:
type: "script"
entry_point: "tests/run_tests.sh"
timeout: 60
result_format: "simple"
python:
type: "unittest"
entry_point: "tests/test_actions.py"
timeout: 120
result_format: "simple"
# Test result expectations
result_path: "tests/results/"
# Minimum passing criteria (100% tests must pass)
min_pass_rate: 1.0
# Block installation if tests fail
on_failure: "block"

View File

@@ -0,0 +1,88 @@
# Timer Sensor
# Monitors time and fires all timer trigger types
name: interval_timer_sensor
ref: core.interval_timer_sensor
description: "Built-in sensor that monitors time and fires timer triggers (interval, cron, and one-shot datetime)"
enabled: true
# Sensor runner type
runner_type: standalone
# Entry point for sensor execution
entry_point: attune-core-timer-sensor
# Trigger types this sensor monitors
trigger_types:
- core.intervaltimer
- core.crontimer
- core.datetimetimer
# Sensor configuration schema (standard JSON Schema format)
parameters:
type: object
properties:
check_interval_seconds:
type: integer
description: "How often to check if triggers should fire (in seconds)"
default: 1
minimum: 1
maximum: 60
required: []
# Poll interval (how often the sensor checks for events)
poll_interval: 1
# Tags for categorization
tags:
- timer
- interval
- system
- builtin
# Metadata
meta:
builtin: true
system: true
description: |
The timer sensor is a built-in system sensor that monitors all timer-based
triggers and fires events according to their schedules. It supports three
timer types:
1. Interval timers: Fire at regular intervals (seconds, minutes, hours, days)
2. Cron timers: Fire based on cron schedule expressions (e.g., "0 0 * * * *")
3. DateTime timers: Fire once at a specific date and time (one-shot)
This sensor uses tokio-cron-scheduler for efficient async scheduling and
runs continuously as part of the Attune sensor service.
# Documentation
examples:
- description: "Interval timer - fires every 10 seconds"
trigger_type: core.intervaltimer
trigger_config:
unit: "seconds"
interval: 10
- description: "Interval timer - fire every 5 minutes"
trigger_type: core.intervaltimer
trigger_config:
unit: "minutes"
interval: 5
- description: "Cron timer - fire every hour on the hour"
trigger_type: core.crontimer
trigger_config:
expression: "0 0 * * * *"
- description: "Cron timer - fire every weekday at 9 AM"
trigger_type: core.crontimer
trigger_config:
expression: "0 0 9 * * 1-5"
timezone: "UTC"
- description: "DateTime timer - fire once at specific time"
trigger_type: core.datetimetimer
trigger_config:
fire_at: "2024-12-31T23:59:59Z"
timezone: "UTC"

193
packs/core/test_core_pack.sh Executable file
View File

@@ -0,0 +1,193 @@
#!/bin/bash
# Automated test script for Core Pack
# Tests all actions to ensure they work correctly
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ACTIONS_DIR="$SCRIPT_DIR/actions"
# Colors for output
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Test counters
TESTS_RUN=0
TESTS_PASSED=0
TESTS_FAILED=0
# Function to print test result
test_result() {
TESTS_RUN=$((TESTS_RUN + 1))
if [ $? -eq 0 ]; then
echo -e "${GREEN}${NC} $1"
TESTS_PASSED=$((TESTS_PASSED + 1))
else
echo -e "${RED}${NC} $1"
TESTS_FAILED=$((TESTS_FAILED + 1))
fi
}
# Function to run a test
run_test() {
local test_name="$1"
shift
echo -n " Testing: $test_name... "
if "$@" > /dev/null 2>&1; then
echo -e "${GREEN}${NC}"
TESTS_PASSED=$((TESTS_PASSED + 1))
else
echo -e "${RED}${NC}"
TESTS_FAILED=$((TESTS_FAILED + 1))
fi
TESTS_RUN=$((TESTS_RUN + 1))
}
echo "========================================="
echo "Core Pack Test Suite"
echo "========================================="
echo ""
# Check if actions directory exists
if [ ! -d "$ACTIONS_DIR" ]; then
echo -e "${RED}ERROR:${NC} Actions directory not found at $ACTIONS_DIR"
exit 1
fi
# Check if scripts are executable
echo "→ Checking script permissions..."
for script in "$ACTIONS_DIR"/*.sh "$ACTIONS_DIR"/*.py; do
if [ -f "$script" ] && [ ! -x "$script" ]; then
echo -e "${YELLOW}WARNING:${NC} $script is not executable, fixing..."
chmod +x "$script"
fi
done
echo -e "${GREEN}${NC} All scripts have correct permissions"
echo ""
# Test core.echo
echo "→ Testing core.echo..."
export ATTUNE_ACTION_MESSAGE="Test message"
export ATTUNE_ACTION_UPPERCASE=false
run_test "basic echo" "$ACTIONS_DIR/echo.sh"
export ATTUNE_ACTION_MESSAGE="test uppercase"
export ATTUNE_ACTION_UPPERCASE=true
OUTPUT=$("$ACTIONS_DIR/echo.sh")
if [ "$OUTPUT" = "TEST UPPERCASE" ]; then
echo -e " Testing: uppercase conversion... ${GREEN}${NC}"
TESTS_PASSED=$((TESTS_PASSED + 1))
else
echo -e " Testing: uppercase conversion... ${RED}${NC} (expected 'TEST UPPERCASE', got '$OUTPUT')"
TESTS_FAILED=$((TESTS_FAILED + 1))
fi
TESTS_RUN=$((TESTS_RUN + 1))
unset ATTUNE_ACTION_MESSAGE ATTUNE_ACTION_UPPERCASE
echo ""
# Test core.sleep
echo "→ Testing core.sleep..."
export ATTUNE_ACTION_SECONDS=1
export ATTUNE_ACTION_MESSAGE="Sleeping..."
run_test "basic sleep (1 second)" "$ACTIONS_DIR/sleep.sh"
# Test invalid seconds
export ATTUNE_ACTION_SECONDS=-1
if "$ACTIONS_DIR/sleep.sh" > /dev/null 2>&1; then
echo -e " Testing: invalid seconds validation... ${RED}${NC} (should have failed)"
TESTS_FAILED=$((TESTS_FAILED + 1))
else
echo -e " Testing: invalid seconds validation... ${GREEN}${NC}"
TESTS_PASSED=$((TESTS_PASSED + 1))
fi
TESTS_RUN=$((TESTS_RUN + 1))
unset ATTUNE_ACTION_SECONDS ATTUNE_ACTION_MESSAGE
echo ""
# Test core.noop
echo "→ Testing core.noop..."
export ATTUNE_ACTION_MESSAGE="Test noop"
export ATTUNE_ACTION_EXIT_CODE=0
run_test "basic noop with exit 0" "$ACTIONS_DIR/noop.sh"
export ATTUNE_ACTION_EXIT_CODE=1
if "$ACTIONS_DIR/noop.sh" > /dev/null 2>&1; then
echo -e " Testing: custom exit code (1)... ${RED}${NC} (should have exited with 1)"
TESTS_FAILED=$((TESTS_FAILED + 1))
else
EXIT_CODE=$?
if [ $EXIT_CODE -eq 1 ]; then
echo -e " Testing: custom exit code (1)... ${GREEN}${NC}"
TESTS_PASSED=$((TESTS_PASSED + 1))
else
echo -e " Testing: custom exit code (1)... ${RED}${NC} (exit code was $EXIT_CODE, expected 1)"
TESTS_FAILED=$((TESTS_FAILED + 1))
fi
fi
TESTS_RUN=$((TESTS_RUN + 1))
unset ATTUNE_ACTION_MESSAGE ATTUNE_ACTION_EXIT_CODE
echo ""
# Test core.http_request (requires Python and requests library)
echo "→ Testing core.http_request..."
# Check if Python is available
if ! command -v python3 &> /dev/null; then
echo -e "${YELLOW}WARNING:${NC} Python 3 not found, skipping HTTP request tests"
else
# Check if requests library is installed
if python3 -c "import requests" 2>/dev/null; then
export ATTUNE_ACTION_URL="https://httpbin.org/get"
export ATTUNE_ACTION_METHOD="GET"
export ATTUNE_ACTION_TIMEOUT=10
run_test "basic GET request" python3 "$ACTIONS_DIR/http_request.py"
export ATTUNE_ACTION_URL="https://httpbin.org/post"
export ATTUNE_ACTION_METHOD="POST"
export ATTUNE_ACTION_JSON_BODY='{"test": "data"}'
run_test "POST with JSON body" python3 "$ACTIONS_DIR/http_request.py"
# Test missing required parameter
unset ATTUNE_ACTION_URL
if python3 "$ACTIONS_DIR/http_request.py" > /dev/null 2>&1; then
echo -e " Testing: missing URL validation... ${RED}${NC} (should have failed)"
TESTS_FAILED=$((TESTS_FAILED + 1))
else
echo -e " Testing: missing URL validation... ${GREEN}${NC}"
TESTS_PASSED=$((TESTS_PASSED + 1))
fi
TESTS_RUN=$((TESTS_RUN + 1))
unset ATTUNE_ACTION_URL ATTUNE_ACTION_METHOD ATTUNE_ACTION_JSON_BODY ATTUNE_ACTION_TIMEOUT
else
echo -e "${YELLOW}WARNING:${NC} Python requests library not found, skipping HTTP tests"
echo " Install with: pip install requests>=2.28.0"
fi
fi
echo ""
# Summary
echo "========================================="
echo "Test Results"
echo "========================================="
echo "Total tests run: $TESTS_RUN"
echo -e "Tests passed: ${GREEN}$TESTS_PASSED${NC}"
if [ $TESTS_FAILED -gt 0 ]; then
echo -e "Tests failed: ${RED}$TESTS_FAILED${NC}"
else
echo -e "Tests failed: ${GREEN}$TESTS_FAILED${NC}"
fi
echo ""
if [ $TESTS_FAILED -eq 0 ]; then
echo -e "${GREEN}✓ All tests passed!${NC}"
exit 0
else
echo -e "${RED}✗ Some tests failed${NC}"
exit 1
fi

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()

View File

@@ -0,0 +1,109 @@
# Cron Timer Trigger
# Fires based on cron schedule expressions
name: crontimer
ref: core.crontimer
description: "Fires based on a cron schedule expression (e.g., '0 0 * * * *' for every hour)"
enabled: true
# Trigger type
type: cron
# Parameter schema - configuration for the trigger instance (standard JSON Schema format)
parameters:
type: object
properties:
expression:
type: string
description: "Cron expression in standard format (second minute hour day month weekday)"
timezone:
type: string
description: "Timezone for cron schedule (e.g., 'UTC', 'America/New_York')"
default: "UTC"
description:
type: string
description: "Human-readable description of the schedule"
required:
- expression
# Payload schema - data emitted when trigger fires
output:
type: object
properties:
type:
type: string
const: cron
description: "Trigger type identifier"
fired_at:
type: string
format: date-time
description: "Timestamp when the trigger fired"
scheduled_at:
type: string
format: date-time
description: "Timestamp when the trigger was scheduled to fire"
expression:
type: string
description: "The cron expression that triggered this event"
timezone:
type: string
description: "Timezone used for scheduling"
next_fire_at:
type: string
format: date-time
description: "Timestamp when the trigger will fire next"
execution_count:
type: integer
description: "Number of times this trigger has fired"
sensor_ref:
type: string
description: "Reference to the sensor that generated this event"
required:
- type
- fired_at
- scheduled_at
- expression
# Tags for categorization
tags:
- timer
- cron
- scheduler
- periodic
# Documentation
examples:
- description: "Fire every hour at the top of the hour"
parameters:
expression: "0 0 * * * *"
description: "Hourly"
- description: "Fire every day at midnight UTC"
parameters:
expression: "0 0 0 * * *"
description: "Daily at midnight"
- description: "Fire every Monday at 9:00 AM"
parameters:
expression: "0 0 9 * * 1"
description: "Weekly on Monday morning"
- description: "Fire every 15 minutes"
parameters:
expression: "0 */15 * * * *"
description: "Every 15 minutes"
- description: "Fire at 8:30 AM on weekdays"
parameters:
expression: "0 30 8 * * 1-5"
description: "Weekday morning"
timezone: "America/New_York"
# Cron format reference
# Field Allowed values Special characters
# second 0-59 * , - /
# minute 0-59 * , - /
# hour 0-23 * , - /
# day of month 1-31 * , - / ?
# month 1-12 or JAN-DEC * , - /
# day of week 0-6 or SUN-SAT * , - / ?

View File

@@ -0,0 +1,88 @@
# Datetime Timer Trigger
# Fires once at a specific date and time
name: datetimetimer
ref: core.datetimetimer
description: "Fires once at a specific date and time"
enabled: true
# Trigger type
type: one_shot
# Parameter schema - configuration for the trigger instance (standard JSON Schema format)
parameters:
type: object
properties:
fire_at:
type: string
description: "ISO 8601 timestamp when the timer should fire (e.g., '2024-12-31T23:59:59Z')"
timezone:
type: string
description: "Timezone for the datetime (e.g., 'UTC', 'America/New_York')"
default: "UTC"
description:
type: string
description: "Human-readable description of when this timer fires"
required:
- fire_at
# Payload schema - data emitted when trigger fires
output:
type: object
properties:
type:
type: string
const: one_shot
description: "Trigger type identifier"
fire_at:
type: string
format: date-time
description: "Scheduled fire time"
fired_at:
type: string
format: date-time
description: "Actual fire time"
timezone:
type: string
description: "Timezone used for scheduling"
delay_ms:
type: integer
description: "Delay in milliseconds between scheduled and actual fire time"
sensor_ref:
type: string
description: "Reference to the sensor that generated this event"
required:
- type
- fire_at
- fired_at
# Tags for categorization
tags:
- timer
- datetime
- one-shot
- scheduler
# Documentation
examples:
- description: "Fire at midnight on New Year's Eve 2024"
parameters:
fire_at: "2024-12-31T23:59:59Z"
description: "New Year's countdown"
- description: "Fire at 3:00 PM EST on a specific date"
parameters:
fire_at: "2024-06-15T15:00:00-05:00"
timezone: "America/New_York"
description: "Afternoon reminder"
- description: "Fire in 1 hour from now (use ISO 8601)"
parameters:
fire_at: "2024-01-20T15:30:00Z"
description: "One-hour reminder"
# Notes:
# - This trigger fires only once and is automatically disabled after firing
# - Use ISO 8601 format for the fire_at parameter
# - The sensor will remove the trigger instance after it fires
# - For recurring timers, use intervaltimer or crontimer instead

View File

@@ -0,0 +1,80 @@
# Interval Timer Trigger
# Fires at regular intervals based on time unit and interval
name: intervaltimer
ref: core.intervaltimer
description: "Fires at regular intervals based on specified time unit and interval"
enabled: true
# Trigger type
type: interval
# Parameter schema - configuration for the trigger instance (standard JSON Schema format)
parameters:
type: object
properties:
unit:
type: string
enum:
- seconds
- minutes
- hours
description: "Time unit for the interval"
default: "seconds"
interval:
type: integer
description: "Number of time units between each trigger"
default: 60
required:
- unit
- interval
# Payload schema - data emitted when trigger fires
output:
type: object
properties:
type:
type: string
const: interval
description: "Trigger type identifier"
interval_seconds:
type: integer
description: "Total interval in seconds"
fired_at:
type: string
format: date-time
description: "Timestamp when the trigger fired"
execution_count:
type: integer
description: "Number of times this trigger has fired"
sensor_ref:
type: string
description: "Reference to the sensor that generated this event"
required:
- type
- interval_seconds
- fired_at
# Tags for categorization
tags:
- timer
- interval
- periodic
- scheduler
# Documentation
examples:
- description: "Fire every 10 seconds"
parameters:
unit: "seconds"
interval: 10
- description: "Fire every 5 minutes"
parameters:
unit: "minutes"
interval: 5
- description: "Fire every hour"
parameters:
unit: "hours"
interval: 1