From 182d3a84a450fe56337f873083a61763d203be89 Mon Sep 17 00:00:00 2001 From: Nick DiZazzo Date: Tue, 12 Aug 2025 14:26:39 -0400 Subject: [PATCH] chore: Update library documentation --- ACTIONS.md | 1021 +++++++------------------ README.md | 129 ++-- docs/API.md | 214 ++++++ docs/ARCHITECTURE.md | 97 +++ docs/QUICKSTART.md | 222 ++++++ docs/TROUBLESHOOTING.md | 237 ++++++ docs/action_update_prompt.md | 48 -- docs/examples/README.md | 23 + docs/features/0001_PLAN.md | 431 ----------- docs/features/0001_REVIEW.md | 253 ------ docs/features/0002_PLAN.md | 1147 ---------------------------- docs/features/0002_REVIEW.md | 289 ------- docs/features/parameter_passing.md | 119 --- testing/README.md | 129 +--- 14 files changed, 1141 insertions(+), 3218 deletions(-) create mode 100644 docs/API.md create mode 100644 docs/ARCHITECTURE.md create mode 100644 docs/QUICKSTART.md create mode 100644 docs/TROUBLESHOOTING.md delete mode 100644 docs/action_update_prompt.md delete mode 100644 docs/features/0001_PLAN.md delete mode 100644 docs/features/0001_REVIEW.md delete mode 100644 docs/features/0002_PLAN.md delete mode 100644 docs/features/0002_REVIEW.md delete mode 100644 docs/features/parameter_passing.md diff --git a/ACTIONS.md b/ACTIONS.md index 1945f77..8fa3f5c 100644 --- a/ACTIONS.md +++ b/ACTIONS.md @@ -1,925 +1,440 @@ -# Built-in Actions Inventory +# Built-in Actions -This document provides a comprehensive inventory of all built-in actions available in the Task Engine. For practical examples and usage patterns, see the example tasks in the `tasks/` directory. +Complete list of available actions. See `tasks/` directory for usage examples. -## Action Categories Summary - -- **File Operations**: 12 actions for comprehensive file/directory management -- **Docker Operations**: 14 actions for container orchestration and management -- **System Management**: 4 actions for system-level operations -- **Utilities**: 4 actions for workflow control and system information - -## File & Directory Operations +## File Operations ### CreateDirectoriesAction -Creates multiple directories with proper path handling. - -**Constructor:** `NewCreateDirectoriesAction(rootPath string, directories []string, logger *slog.Logger)` - -**Parameters:** - -- `rootPath`: Base directory path -- `directories`: List of directory names to create -- `logger`: Logger instance +Creates multiple directories. -**See Example:** `tasks.NewFileOperationsTask()` - Demonstrates directory creation as part of a complete file operations workflow. +```go +file.NewCreateDirectoriesAction(logger).WithParameters( + engine.StaticParameter{Value: "/path/to/root"}, + engine.StaticParameter{Value: []string{"src", "tests", "docs"}}, +) +``` ### WriteFileAction -Writes content to files with optional buffering and overwrite control. - -**Constructor:** `NewWriteFileAction(filePath string, content []byte, overwrite bool, inputBuffer *bytes.Buffer, logger *slog.Logger)` - -**Parameters:** +Writes content to files. -- `filePath`: Target file path -- `content`: Content to write (can be nil if using inputBuffer) -- `overwrite`: Whether to overwrite existing files -- `inputBuffer`: Alternative content source (can be nil) -- `logger`: Logger instance - -**See Example:** `tasks.NewFileOperationsTask()` - Shows file creation with various content types and configurations. +```go +file.NewWriteFileAction(logger).WithParameters( + engine.StaticParameter{Value: "/path/to/file"}, + engine.StaticParameter{Value: []byte("content")}, + true, // overwrite + nil, // inputBuffer +) +``` ### ReadFileAction -Reads file contents and stores them in a provided byte array buffer. - -**Constructor:** `NewReadFileAction(filePath string, outputBuffer *[]byte, logger *slog.Logger)` - -**Parameters:** - -- `filePath`: Path to the file to read -- `outputBuffer`: Pointer to byte array where file contents will be stored -- `logger`: Logger instance - -**Features:** - -- Validates file existence before reading -- Ensures the path is a regular file (not a directory) -- Handles permission errors gracefully -- Supports reading files of any size (limited by available memory) -- Preserves all file content including special characters and unicode +Reads file contents. -**See Example:** `tasks.NewReadFileOperationsTask()` - Demonstrates file reading with error handling and content processing. +```go +var content []byte +file.NewReadFileAction("/path/to/file", &content, logger) +``` ### CompressFileAction -Compresses a file using the specified compression algorithm (currently supports gzip). - -**Constructor:** `NewCompressFileAction(sourcePath string, destinationPath string, compressionType CompressionType, logger *slog.Logger)` - -**Parameters:** - -- `sourcePath`: Source file path to compress -- `destinationPath`: Destination path for the compressed file -- `compressionType`: Type of compression to use (e.g., `file.GzipCompression`) -- `logger`: Logger instance - -**Supported Compression Types:** - -- `file.GzipCompression`: Gzip compression (`.gz` files) - -**Features:** +Compresses files (gzip). -- Validates source file existence and type -- Creates destination directories automatically -- Provides compression ratio information -- Handles large files efficiently -- Supports empty files - -**See Example:** `tasks.NewCompressionOperationsTask()` - Shows compression workflows with multiple files and auto-detection. +```go +file.NewCompressFileAction(logger).WithParameters( + engine.StaticParameter{Value: "/source/file"}, + engine.StaticParameter{Value: "/dest/file.gz"}, + file.GzipCompression, +) +``` ### DecompressFileAction -Decompresses a file using the specified compression algorithm. Supports auto-detection from file extension. - -**Constructor:** `NewDecompressFileAction(sourcePath string, destinationPath string, compressionType CompressionType, logger *slog.Logger)` - -**Parameters:** - -- `sourcePath`: Source compressed file path -- `destinationPath`: Destination path for the decompressed file -- `compressionType`: Type of compression (can be empty for auto-detection) -- `logger`: Logger instance - -**Auto-Detection:** -When `compressionType` is empty, the action will auto-detect the compression type from the file extension: - -- `.gz`, `.gzip` → Gzip compression +Decompresses files with auto-detection. -**Features:** - -- Validates source file existence and type -- Creates destination directories automatically -- Provides compression ratio information -- Supports auto-detection from file extensions -- Handles large files efficiently - -**See Example:** `tasks.NewCompressionWithAutoDetectTask()` - Demonstrates auto-detection capabilities. +```go +file.NewDecompressFileAction(logger).WithParameters( + engine.StaticParameter{Value: "/source/file.gz"}, + engine.StaticParameter{Value: "/dest/file"}, + "", // auto-detect +) +``` ### CopyFileAction -Copies files and directories with optional recursive copying and directory creation. +Copies files and directories. -**Constructor:** `NewCopyFileAction(source string, destination string, createDirs bool, recursive bool, logger *slog.Logger)` - -**Parameters:** - -- `source`: Source file or directory path -- `destination`: Destination file or directory path -- `createDirs`: Whether to create destination directories -- `recursive`: Whether to copy directories recursively (uses -R flag equivalent) -- `logger`: Logger instance - -**See Example:** `tasks.NewFileOperationsTask()` - Shows file copying as part of backup and workflow operations. +```go +file.NewCopyFileAction(logger).WithParameters( + engine.StaticParameter{Value: "/source"}, + engine.StaticParameter{Value: "/dest"}, + true, // createDirs + false, // recursive +) +``` ### DeletePathAction -Safely deletes files and directories with optional recursive deletion and dry-run support. - -**Constructor:** `NewDeletePathAction(path string, recursive bool, dryRun bool, logger *slog.Logger)` - -**Parameters:** - -- `path`: Path to file or directory to delete -- `recursive`: Whether to delete directories recursively -- `dryRun`: Whether to simulate deletion without actually deleting -- `logger`: Logger instance +Safely deletes files and directories. -**Features:** - -- Supports both files and directories -- Recursive directory deletion with comprehensive logging -- Dry-run mode for safe testing -- Detailed deletion planning and statistics -- Handles symlinks and special files -- Graceful handling of non-existent paths - -**See Example:** `tasks.NewFileOperationsTask()` - Demonstrates safe file deletion and cleanup operations. +```go +file.NewDeletePathAction(logger).WithParameters( + engine.StaticParameter{Value: "/path/to/delete"}, + true, // recursive + false, // dryRun +) +``` ### ReplaceLinesAction -Replaces text in files using regex patterns. +Replaces text using regex patterns. -**Constructor:** `NewReplaceLinesAction(filePath string, pattern string, replacement string, logger *slog.Logger)` - -**Parameters:** - -- `filePath`: Target file path -- `pattern`: Regex pattern to match -- `replacement`: Replacement text -- `logger`: Logger instance - -**See Example:** `tasks.NewFileOperationsTask()` - Shows text replacement in configuration files and source code. +```go +file.NewReplaceLinesAction(logger).WithParameters( + engine.StaticParameter{Value: "/path/to/file"}, + map[*regexp.Regexp]engine.ActionParameter{ + regexp.MustCompile("old"): engine.StaticParameter{Value: "new"}, + }, +) +``` ### ChangeOwnershipAction -Changes file/directory ownership using chown command. - -**Constructor:** `NewChangeOwnershipAction(path string, owner string, group string, recursive bool, logger *slog.Logger)` +Changes file ownership. -**Parameters:** - -- `path`: Path to file/directory -- `owner`: New owner (can be empty) -- `group`: New group (can be empty) -- `recursive`: Whether to apply recursively -- `logger`: Logger instance +```go +file.NewChangeOwnershipAction(logger).WithParameters( + engine.StaticParameter{Value: "/path"}, + engine.StaticParameter{Value: "user"}, + engine.StaticParameter{Value: "group"}, + true, // recursive +) +``` ### ChangePermissionsAction -Changes file/directory permissions using chmod command. - -**Constructor:** `NewChangePermissionsAction(path string, permissions string, recursive bool, logger *slog.Logger)` +Changes file permissions. -**Parameters:** - -- `path`: Path to file/directory -- `permissions`: Permissions (octal like "755" or symbolic like "u+x") -- `recursive`: Whether to apply recursively -- `logger`: Logger instance +```go +file.NewChangePermissionsAction(logger).WithParameters( + engine.StaticParameter{Value: "/path"}, + engine.StaticParameter{Value: "755"}, + true, // recursive +) +``` ### MoveFileAction -Moves/renames files and directories using mv command. - -**Constructor:** `NewMoveFileAction(source string, destination string, createDirs bool, logger *slog.Logger)` +Moves/renames files. -**Parameters:** - -- `source`: Source path -- `destination`: Destination path -- `createDirs`: Whether to create destination directories -- `logger`: Logger instance +```go +file.NewMoveFileAction(logger).WithParameters( + engine.StaticParameter{Value: "/source"}, + engine.StaticParameter{Value: "/dest"}, + true, // createDirs +) +``` ### ExtractFileAction -Extracts files from archive formats (tar, zip) with comprehensive security features and auto-detection. - -**Constructor:** `NewExtractFileAction(sourcePath string, destinationPath string, archiveType ArchiveType, logger *slog.Logger)` - -**Parameters:** - -- `sourcePath`: Path to the archive file to extract -- `destinationPath`: Directory where files will be extracted -- `archiveType`: Type of archive (can be empty for auto-detection) -- `logger`: Logger instance - -**Supported Archive Types:** - -- `file.TarArchive`: Tar archives (`.tar`) -- `file.ZipArchive`: Zip archives (`.zip`) -- `file.AutoDetect`: Auto-detect from file extension - -**Auto-Detection:** -When `archiveType` is `file.AutoDetect`, the action will auto-detect the archive type from the file extension: - -- `.tar` → Tar archive -- `.zip` → Zip archive - -**Security Features:** +Extracts archives (tar, zip) with security features. -- **Path Traversal Protection**: Validates and sanitizes file paths to prevent zip slip attacks -- **Decompression Bomb Protection**: Limits file sizes to prevent memory exhaustion attacks -- **Integer Overflow Protection**: Safe permission setting with bit masking -- **Insecure Permissions Prevention**: Uses secure default permissions (0600 for files, 0750 for directories) - -**Features:** - -- Validates source file existence and type -- Creates destination directories automatically -- Supports both tar and zip archive formats -- Auto-detection from file extensions -- Comprehensive security measures -- Handles large archives efficiently -- Preserves file permissions (when safe) -- Detailed error reporting - -**Error Handling:** - -- Returns error if compressed archives (`.tar.gz`) are passed directly -- Validates archive integrity before extraction -- Handles corrupted or invalid archives gracefully -- Provides detailed error messages for troubleshooting - -**See Example:** `tasks.NewExtractOperationsTask()` - Demonstrates archive extraction workflows with security considerations. +```go +file.NewExtractFileAction(logger).WithParameters( + engine.StaticParameter{Value: "/archive.tar"}, + engine.StaticParameter{Value: "/extract/dir"}, + file.AutoDetect, // or file.TarArchive, file.ZipArchive +) +``` ## Docker Operations -### GetDockerStatusAction - -Retrieves the state of specific Docker containers by ID or name. +### GetContainerStateAction -**Constructor:** `NewGetContainerStateAction(logger *slog.Logger, containerIdentifiers ...string)` +Gets container status by ID or name. -**Parameters:** - -- `logger`: Logger instance -- `containerIdentifiers`: Variable number of container IDs or names to query - -**Returns:** `ContainerState` struct with ID, names, image, and status information - -**Features:** - -- Supports multiple container queries in a single action -- Handles containers with multiple names/aliases -- Robust JSON parsing with error recovery -- Returns structured container information +```go +docker.NewGetContainerStateAction(logger, "container1", "container2") +``` ### GetAllContainersStateAction -Retrieves the state of all Docker containers on the system. - -**Constructor:** `NewGetAllContainersStateAction(logger *slog.Logger)` - -**Parameters:** - -- `logger`: Logger instance - -**Returns:** Array of `ContainerState` structs for all containers - -**Features:** - -- Comprehensive container enumeration -- Includes stopped, running, and paused containers -- Efficient JSON parsing for large container lists - -### ContainerState Struct - -Container state information is returned in a structured format: +Gets status of all containers. ```go -type ContainerState struct { - ID string // Container ID - Names []string // Container names (can be multiple) - Image string // Container image - Status string // Container status with full state information -} +docker.NewGetAllContainersStateAction(logger) ``` -**Supported Container States:** - -- `created` - Container created but not started -- `restarting` - Container is restarting (with restart policy) -- `running` - Container is running (shows as "Up X time") -- `removing` - Container is being removed -- `paused` - Container is paused -- `exited` - Container has exited (shows as "Exited (code) X time ago") -- `dead` - Container is dead (failed to start or was killed) - -**Status Examples:** - -- `"Up 2 hours"` - Running for 2 hours -- `"Exited (0) 1 hour ago"` - Exited with code 0, 1 hour ago -- `"Paused"` - Currently paused -- `"Created"` - Created but not started -- `"Restarting (1) 2 minutes ago"` - Restarting, attempt 1, 2 minutes ago -- `"Dead"` - Container is dead -- `"Removing"` - Container is being removed - ### DockerComposeUpAction -Starts Docker Compose services with optional working directory. - -**Constructor:** `NewDockerComposeUpAction(services []string, workingDir string, logger *slog.Logger)` +Starts Docker Compose services. -**Parameters:** - -- `services`: List of services to start (empty for all) -- `workingDir`: Working directory for docker-compose command -- `logger`: Logger instance - -**See Example:** `tasks.NewDockerSetupTask()` - Demonstrates Docker environment setup and service management. +```go +docker.NewDockerComposeUpAction(logger).WithParameters( + engine.StaticParameter{Value: []string{"web", "db"}}, + engine.StaticParameter{Value: "/path/to/compose"}, +) +``` ### DockerComposeDownAction -Stops Docker Compose services with optional working directory. +Stops Docker Compose services. -**Constructor:** `NewDockerComposeDownAction(services []string, workingDir string, logger *slog.Logger)` - -**Parameters:** - -- `services`: List of services to stop (empty for all) -- `workingDir`: Working directory for docker-compose command -- `logger`: Logger instance +```go +docker.NewDockerComposeDownAction(logger).WithParameters( + engine.StaticParameter{Value: []string{"web"}}, + engine.StaticParameter{Value: "/path/to/compose"}, +) +``` ### DockerComposeExecAction -Executes commands in Docker Compose containers. +Executes commands in containers. -**Constructor:** `NewDockerComposeExecAction(service string, command []string, workingDir string, logger *slog.Logger)` - -**Parameters:** - -- `service`: Target service name -- `command`: Command to execute -- `workingDir`: Working directory for docker-compose command -- `logger`: Logger instance +```go +docker.NewDockerComposeExecAction(logger).WithParameters( + engine.StaticParameter{Value: "web"}, + engine.StaticParameter{Value: []string{"ls", "-la"}}, + engine.StaticParameter{Value: "/path/to/compose"}, +) +``` ### DockerRunAction -Runs Docker containers with flexible configuration. +Runs Docker containers. -**Constructor:** `NewDockerRunAction(image string, command []string, options []string, inputBuffer *bytes.Buffer, logger *slog.Logger)` - -**Parameters:** - -- `image`: Docker image name -- `command`: Command to run in container -- `options`: Docker run options -- `inputBuffer`: Input buffer for container -- `logger`: Logger instance +```go +docker.NewDockerRunAction(logger).WithParameters( + engine.StaticParameter{Value: "nginx:latest"}, + engine.StaticParameter{Value: []string{"nginx", "-g", "daemon off;"}}, + engine.StaticParameter{Value: []string{"-p", "8080:80"}, + nil, // inputBuffer +) +``` ### CheckContainerHealthAction -Performs health checks on containers with retry logic. +Performs health checks with retries. -**Constructor:** `NewCheckContainerHealthAction(containerName string, maxRetries int, retryDelay time.Duration, workingDir string, logger *slog.Logger)` - -**Parameters:** - -- `containerName`: Name of container to check -- `maxRetries`: Maximum number of retry attempts -- `retryDelay`: Delay between retries -- `workingDir`: Working directory for docker commands -- `logger`: Logger instance +```go +docker.NewCheckContainerHealthAction(logger).WithParameters( + engine.StaticParameter{Value: "container"}, + engine.StaticParameter{Value: 3}, // maxRetries + engine.StaticParameter{Value: time.Second}, // retryDelay + engine.StaticParameter{Value: "/workdir"}, +) +``` ### DockerLoadAction -Loads Docker images from tar archive files with support for platform filtering and quiet mode. - -**Constructor:** `NewDockerLoadAction(logger *slog.Logger, tarFilePath string, options ...DockerLoadOption)` - -**Parameters:** - -- `logger`: Logger instance -- `tarFilePath`: Path to the tar archive file containing Docker images -- `options`: Optional configuration options (see below) - -**Options:** - -- `WithPlatform(platform string)`: Load only the specified platform variant (e.g., "linux/amd64", "linux/arm64") -- `WithQuiet()`: Suppress the load output - -**Features:** - -- Loads images from tar archive files using `docker load -i` -- Supports platform-specific loading for multi-architecture images -- Quiet mode for reduced output in automated workflows -- Parses and stores loaded image names and IDs -- Comprehensive error handling with detailed error messages -- Flexible configuration through functional options pattern - -**Returns:** Array of loaded image names/IDs in the `LoadedImages` field +Loads images from tar files. -**See Example:** `tasks.NewDockerLoadTask()` - Demonstrates various Docker load operations including platform-specific loading and batch operations. +```go +docker.NewDockerLoadAction(logger, "/path/to/image.tar") +// With options: +docker.NewDockerLoadAction(logger, "/path/to/image.tar", + docker.WithPlatform("linux/amd64"), + docker.WithQuiet(), +) +``` ### DockerImageRmAction -Removes Docker images by name/tag or ID with support for force removal and pruning control. - -**Constructors:** - -- `NewDockerImageRmByNameAction(logger *slog.Logger, imageName string, options ...DockerImageRmOption)`: Remove image by name and tag -- `NewDockerImageRmByIDAction(logger *slog.Logger, imageID string, options ...DockerImageRmOption)`: Remove image by ID - -**Parameters:** - -- `logger`: Logger instance -- `imageName`: Image name and tag (e.g., "nginx:latest", "my-registry.com/namespace/image:v1.0") -- `imageID`: Image ID (e.g., "sha256:abc123def456789") -- `options`: Optional configuration options (see below) - -**Options:** - -- `WithForce()`: Force the removal of the image (equivalent to `docker image rm -f`) -- `WithNoPrune()`: Prevent removal of untagged parent images (equivalent to `docker image rm --no-prune`) - -**Features:** +Removes Docker images. -- Two distinct constructors for different use cases (name/tag vs ID) -- Supports force removal for images that might be in use -- Controls pruning behavior for parent layers -- Parses and stores removed image names and IDs -- Comprehensive error handling with detailed error messages -- Flexible configuration through functional options pattern -- Handles registry images with complex naming patterns - -**Returns:** Array of removed image names/IDs in the `RemovedImages` field - -**See Example:** `tasks.NewDockerImageRmTask()` - Demonstrates various Docker image removal operations including force removal and cleanup workflows. +```go +// By name +docker.NewDockerImageRmByNameAction(logger, "nginx:latest") +// By ID +docker.NewDockerImageRmByIDAction(logger, "sha256:abc123") +// With options: +docker.NewDockerImageRmByNameAction(logger, "nginx:latest", + docker.WithForce(), + docker.WithNoPrune(), +) +``` ### DockerImageListAction -Lists Docker images with comprehensive metadata including repository, tag, image ID, creation time, and size. - -**Constructor:** `NewDockerImageListAction(logger *slog.Logger, options ...DockerImageListOption)` - -**Parameters:** - -- `logger`: Logger instance -- `options`: Optional configuration options (see below) - -**Options:** - -- `WithAll()`: Show all images (including intermediate images) -- `WithDigests()`: Show digests -- `WithFilter(filter string)`: Filter output based on conditions -- `WithFormat(format string)`: Use a custom template for output -- `WithNoTrunc()`: Don't truncate output -- `WithQuiet()`: Only show image IDs - -**Features:** - -- Lists all Docker images with detailed metadata -- Supports filtering by various criteria (dangling, label, before, since, etc.) -- Custom output formatting using Go templates -- Handles dangling images (`:`) -- Parses and stores structured image information -- Comprehensive error handling with detailed error messages -- Flexible configuration through functional options pattern - -**Returns:** Array of `DockerImage` structs with repository, tag, image ID, created time, and size - -**DockerImage Structure:** +Lists Docker images. ```go -type DockerImage struct { - Repository string - Tag string - ImageID string - Created string - Size string -} +docker.NewDockerImageListAction(logger) +// With options: +docker.NewDockerImageListAction(logger, + docker.WithAll(), + docker.WithDigests(), + docker.WithFilter("dangling=true"), +) ``` -**See Example:** `tasks.NewDockerImageListTask()` - Demonstrates various Docker image listing operations including filtering and formatting. - ### DockerComposeLsAction -Lists Docker Compose stacks with their status and configuration files. - -**Constructor:** `NewDockerComposeLsAction(logger *slog.Logger, options ...DockerComposeLsOption)` - -**Parameters:** - -- `logger`: Logger instance -- `options`: Optional configuration options (see below) - -**Options:** - -- `WithAll()`: Show all stacks (including stopped ones) -- `WithFilter(filter string)`: Filter output based on conditions -- `WithFormat(format string)`: Use a custom template for output -- `WithQuiet()`: Only show stack names -- `WithWorkingDir(workingDir string)`: Set working directory for docker-compose command - -**Features:** - -- Lists all Docker Compose stacks with their current status -- Shows configuration files used by each stack -- Supports filtering by stack name or other criteria -- Custom output formatting using Go templates -- Parses and stores structured stack information -- Comprehensive error handling with detailed error messages -- Flexible configuration through functional options pattern - -**Returns:** Array of `ComposeStack` structs with name, status, and config files - -**ComposeStack Structure:** +Lists Docker Compose stacks. ```go -type ComposeStack struct { - Name string - Status string - ConfigFiles string -} +docker.NewDockerComposeLsAction(logger) +// With options: +docker.NewDockerComposeLsAction(logger, + docker.WithAll(), + docker.WithWorkingDir("/path/to/compose"), +) ``` -**See Example:** `tasks.NewDockerComposeLsTask()` - Demonstrates Docker Compose stack listing operations including filtering and status monitoring. - ### DockerComposePsAction -Lists services for Docker Compose stacks with detailed container information. - -**Constructor:** `NewDockerComposePsAction(services []string, logger *slog.Logger, options ...DockerComposePsOption)` - -**Parameters:** - -- `services`: List of services to list (empty for all) -- `logger`: Logger instance -- `options`: Optional configuration options (see below) - -**Options:** - -- `WithAll()`: Show all containers (including stopped ones) -- `WithFilter(filter string)`: Filter output based on conditions -- `WithFormat(format string)`: Use a custom template for output -- `WithQuiet()`: Only show container names -- `WithWorkingDir(workingDir string)`: Set working directory for docker-compose command - -**Features:** - -- Lists services for Docker Compose stacks with detailed information -- Shows container name, image, command, service name, status, and ports -- Supports filtering by service status or other criteria -- Custom output formatting using Go templates -- Parses and stores structured service information -- Comprehensive error handling with detailed error messages -- Flexible configuration through functional options pattern - -**Returns:** Array of `ComposeService` structs with container details - -**ComposeService Structure:** +Lists services in Docker Compose stacks. ```go -type ComposeService struct { - Name string - Image string - Command string - ServiceName string - Status string - Ports string -} +docker.NewDockerComposePsAction(logger, []string{"web"}) +// With options: +docker.NewDockerComposePsAction(logger, []string{"web"}, + docker.WithAll(), + docker.WithWorkingDir("/path/to/compose"), +) ``` -**See Example:** `tasks.NewDockerComposePsTask()` - Demonstrates Docker Compose service listing operations including status monitoring and port mapping. - ### DockerPsAction -Lists Docker containers with comprehensive metadata including container ID, image, command, status, ports, and names. - -**Constructor:** `NewDockerPsAction(logger *slog.Logger, options ...DockerPsOption)` - -**Parameters:** - -- `logger`: Logger instance -- `options`: Optional configuration options (see below) - -**Options:** - -- `WithPsAll()`: Show all containers (including stopped ones) -- `WithPsFilter(filter string)`: Filter output based on conditions -- `WithPsFormat(format string)`: Use a custom template for output -- `WithPsLast(n int)`: Show n last created containers -- `WithPsLatest()`: Show the latest created container -- `WithPsNoTrunc()`: Don't truncate output -- `WithPsQuiet()`: Only show container IDs -- `WithPsSize()`: Display total file sizes - -**Features:** - -- Lists all Docker containers with detailed metadata -- Supports filtering by various criteria (status, label, ancestor, etc.) -- Custom output formatting using Go templates -- Shows container size information when requested -- Parses and stores structured container information -- Comprehensive error handling with detailed error messages -- Flexible configuration through functional options pattern - -**Returns:** Array of `Container` structs with container details - -**Container Structure:** +Lists Docker containers. ```go -type Container struct { - ContainerID string - Image string - Command string - Created string - Status string - Ports string - Names string -} +docker.NewDockerPsAction(logger) +// With options: +docker.NewDockerPsAction(logger, + docker.WithPsAll(), + docker.WithPsFilter("status=running"), +) ``` -**See Example:** `tasks.NewDockerPsTask()` - Demonstrates various Docker container listing operations including filtering, formatting, and status monitoring. - ### DockerPullAction -Pulls Docker images with support for single architecture specifications and platform options. - -**Constructor:** `NewDockerPullAction(logger *slog.Logger, images map[string]ImageSpec, options ...DockerPullOption)` - -**Parameters:** - -- `logger`: Logger instance -- `images`: Map of image names to `ImageSpec` configurations -- `options`: Optional configuration options (see below) - -**ImageSpec Structure:** +Pulls Docker images. ```go -type ImageSpec struct { - Image string - Tag string - Architecture string +images := map[string]docker.ImageSpec{ + "nginx": {Image: "nginx", Tag: "latest", Architecture: "amd64"}, } +docker.NewDockerPullAction(logger, images) +// With options: +docker.NewDockerPullAction(logger, images, + docker.WithAllTags(), + docker.WithPullPlatform("linux/amd64"), +) ``` -**Options:** - -- `WithAllTags()`: Pull all tags for the specified images -- `WithPullQuietOutput()`: Suppress verbose output -- `WithPullPlatform(platform string)`: Specify platform for pulled images - -**Features:** - -- Pulls Docker images with architecture-specific platform targeting -- Supports multiple images in a single action -- Platform override options for cross-platform compatibility -- Quiet mode for reduced output in automated workflows -- Comprehensive error handling with detailed error messages -- Tracks successfully pulled and failed images -- Flexible configuration through functional options pattern - -**Returns:** Arrays of successfully pulled and failed image names in `PulledImages` and `FailedImages` fields - -**See Example:** `tasks.NewDockerPullTask()` - Demonstrates Docker image pulling operations with various configurations. - ### DockerPullMultiArchAction -Pulls Docker images for multiple architectures in a single action with robust error handling. - -**Constructor:** `NewDockerPullMultiArchAction(logger *slog.Logger, multiArchImages map[string]MultiArchImageSpec, options ...DockerPullOption)` - -**Parameters:** - -- `logger`: Logger instance -- `multiArchImages`: Map of image names to `MultiArchImageSpec` configurations -- `options`: Optional configuration options (see below) - -**MultiArchImageSpec Structure:** +Pulls images for multiple architectures. ```go -type MultiArchImageSpec struct { - Image string - Tag string - Architectures []string +images := map[string]docker.MultiArchImageSpec{ + "nginx": { + Image: "nginx", + Tag: "latest", + Architectures: []string{"amd64", "arm64"}, + }, } +docker.NewDockerPullMultiArchAction(logger, images) ``` -**Options:** - -- `WithAllTags()`: Pull all tags for the specified images -- `WithPullQuietOutput()`: Suppress verbose output -- `WithPullPlatform(platform string)`: Override platform for all architectures - -**Features:** - -- Pulls the same image for multiple architectures (e.g., amd64, arm64, arm/v7) -- Robust error handling with partial success support -- Continues pulling other architectures even if some fail -- Detailed logging per architecture -- Platform override capability for all architectures -- Tracks successfully pulled and failed images -- Flexible configuration through functional options pattern - -**Error Handling:** - -- **Partial Success**: If some architectures fail, others still succeed -- **Complete Failure**: Only fails if ALL architectures fail for an image -- **Warning Messages**: Alerts when partial failures occur - -**Returns:** Arrays of successfully pulled and failed image names in `PulledImages` and `FailedImages` fields - -**See Example:** `tasks.NewDockerPullMultiArchTask()` - Demonstrates multi-architecture Docker image pulling operations. - ### DockerGenericAction -Executes generic Docker commands with flexible arguments. - -**Constructor:** `NewDockerGenericAction(args []string, logger *slog.Logger)` - -**Parameters:** +Executes generic Docker commands. -- `args`: Docker command arguments -- `logger`: Logger instance +```go +docker.NewDockerGenericAction(logger, []string{"images", "-q"}) +``` ## System Management ### GetServiceStatusAction -Retrieves the status of one or more systemd services using `systemctl show` with specific properties for reliable parsing. - -**Constructor:** `NewGetServiceStatusAction(logger *slog.Logger, serviceNames ...string)` - -**Parameters:** - -- `logger`: Logger instance -- `serviceNames`: Variable number of service names to check - -**Features:** - -- Uses `systemctl show` with specific properties for reliable, machine-readable output -- Handles multiple services in a single action -- Retrieves comprehensive service information including: - - Service description and documentation - - Loaded state and vendor information - - Active state and sub-status - - Service path and configuration details -- Gracefully handles non-existent services -- More efficient and reliable than parsing human-readable `systemctl status` output -- Handles various service states (running, exited, dead, etc.) -- Processes each service individually to avoid mixed output issues - -**ServiceStatus Structure:** +Gets systemd service status. ```go -type ServiceStatus struct { - Name string `json:"name"` - Loaded string `json:"loaded"` - Active string `json:"active"` - Sub string `json:"sub"` - Description string `json:"description"` - Path string `json:"path"` - Vendor string `json:"vendor"` - Exists bool `json:"exists"` -} +system.NewGetServiceStatusAction(logger, "nginx", "mysql") ``` -**See Example:** `tasks.NewServiceStatusTask()` - Demonstrates service status checking and health monitoring. - ### ManageServiceAction -Controls systemd services (start/stop/restart). - -**Constructor:** `NewManageServiceAction(serviceName string, action string, logger *slog.Logger)` - -**Parameters:** +Controls systemd services. -- `serviceName`: Name of the systemd service -- `action`: Action to perform ("start", "stop", "restart") -- `logger`: Logger instance - -**See Example:** `tasks.NewSystemManagementTask()` - Demonstrates system service management operations. +```go +system.NewManageServiceAction(logger).WithParameters( + engine.StaticParameter{Value: "nginx"}, + engine.StaticParameter{Value: "restart"}, +) +``` ### ShutdownAction -Performs system shutdown or restart with optional delays. - -**Constructor:** `NewShutdownAction(shutdownType string, delay string, logger *slog.Logger)` +Shuts down or restarts system. -**Parameters:** - -- `shutdownType`: Type of shutdown ("shutdown", "restart") -- `delay`: Delay before shutdown (e.g., "5", "now") -- `logger`: Logger instance +```go +system.NewShutdownAction(logger).WithParameters( + engine.StaticParameter{Value: "restart"}, + engine.StaticParameter{Value: "5"}, // delay in minutes +) +``` ### UpdatePackagesAction -Installs packages using the appropriate package manager for the operating system. Supports Debian-based Linux (apt) and macOS (Homebrew). - -**Constructor:** `NewUpdatePackagesAction(packageNames []string, logger *slog.Logger)` - -**Parameters:** - -- `packageNames`: List of package names to install -- `logger`: Logger instance - -**Supported Platforms:** - -- **Debian-based Linux**: Uses `apt install -y` (automatically updates package list first) -- **macOS**: Uses `brew install` -- **Auto-detection**: Automatically detects the appropriate package manager +Installs packages (apt/brew). -**Features:** - -- Non-interactive installation (uses `-y` flag for apt) -- Automatic package list updates for apt -- Cross-platform compatibility -- Comprehensive error handling -- Detailed logging of installation progress - -**See Example:** `tasks.NewPackageManagementTask()` - Shows package installation workflows for different platforms. +```go +system.NewUpdatePackagesAction(logger, []string{"git", "curl"}) +``` ## Utilities ### WaitAction -Waits for a specified duration with context cancellation support. +Waits for specified duration. -**Constructor:** `NewWaitAction(duration time.Duration, logger *slog.Logger)` - -**Parameters:** - -- `duration`: Duration to wait -- `logger`: Logger instance - -**See Example:** `tasks.NewUtilityOperationsTask()` - Demonstrates utility operations including timing and delays. +```go +utility.NewWaitAction(logger).WithParameters( + engine.StaticParameter{Value: time.Second * 5}, +) +``` ### PrerequisiteCheckAction -Performs conditional execution based on custom check functions. - -**Constructor:** `NewPrerequisiteCheckAction(checkFunc func(context.Context) (bool, error), logger *slog.Logger)` +Conditional execution based on custom checks. -**Parameters:** - -- `checkFunc`: Function that returns true if prerequisites are met -- `logger`: Logger instance +```go +utility.NewPrerequisiteCheckAction(logger).WithParameters( + engine.StaticParameter{Value: func(ctx context.Context) (bool, error) { + // custom check logic + return true, nil + }}, +) +``` ### FetchInterfacesAction -Retrieves network interface information from the system. - -**Constructor:** `NewFetchInterfacesAction(logger *slog.Logger)` +Gets network interface information. -**Parameters:** - -- `logger`: Logger instance +```go +utility.NewFetchInterfacesAction(logger) +``` ### ReadMACAddressAction -Reads the MAC address of a specific network interface from the system. - -**Constructor:** `NewReadMacAction(netInterface string, logger *slog.Logger)` - -**Parameters:** - -- `netInterface`: Name of the network interface (e.g., "eth0", "wlan0") -- `logger`: Logger instance +Reads MAC address of network interface. -**Features:** - -- Reads MAC address from `/sys/class/net/{interface}/address` -- Trims whitespace from the result -- Stores MAC address in the action for later retrieval - -## Example Tasks +```go +utility.NewReadMacAction(logger).WithParameters( + engine.StaticParameter{Value: "eth0"}, +) +``` -For practical examples and complete workflows, see the following task functions in the `tasks/` directory: +## Examples -- **File Operations**: `tasks.NewFileOperationsTask()` - Complete file management workflow -- **Compression**: `tasks.NewCompressionOperationsTask()` - File compression and decompression -- **Package Management**: `tasks.NewPackageManagementTask()` - Cross-platform package installation -- **System Management**: `tasks.NewSystemManagementTask()` - System-level operations -- **Utility Operations**: `tasks.NewUtilityOperationsTask()` - Utility and helper operations -- **Docker Setup**: `tasks.NewDockerSetupTask()` - Docker environment configuration -- **Container State**: `tasks.NewContainerStateTask()` - Container monitoring and state management -- **Docker Pull**: `tasks.NewDockerPullTask()` - Docker image pulling operations -- **Docker Pull Multi-Arch**: `tasks.NewDockerPullMultiArchTask()` - Multi-architecture image pulling -- **Extract Operations**: `tasks.NewExtractOperationsTask()` - Archive extraction with security features +See `tasks/` directory for complete workflows: -Each example task demonstrates real-world usage patterns and can be used as a starting point for your own workflows. +- `NewFileOperationsTask()` - File management pipeline +- `NewDockerSetupTask()` - Docker environment setup +- `NewPackageManagementTask()` - Package installation +- `NewExtractOperationsTask()` - Archive extraction diff --git a/README.md b/README.md index eb5522a..bf3581d 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,9 @@ # Task Engine [![CI/CD Pipeline](https://github.com/ndizazzo/task-engine/actions/workflows/ci.yml/badge.svg)](https://github.com/ndizazzo/task-engine/actions/workflows/ci.yml) -[![codecov](https://codecov.io/gh/ndizazzo/task-engine/graph/badge.svg?token=O020V4C6TV)](https://codecov.io/gh/ndizazzo/task-engine) +[![codecov](https://codecov.io/gh/ndizazzo/task-engine/graph/badge.svg?token=O020V4C6TV)](https://codecov.io/gh/ndizazzo/task-engine/graph/badge.svg?token=O020V4C6TV) -A powerful, type-safe Go task execution framework with built-in actions for file operations, Docker management, system administration, and more. - -## Installation - -Add the module to your Go project: - -```bash -go get github.com/ndizazzo/task-engine -``` +A Go task execution framework with built-in actions for file operations, Docker management, and system administration. ## Quick Start @@ -30,68 +22,71 @@ import ( func main() { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) - // Create a simple task using the file operations example + // Create and run a task task := tasks.NewFileOperationsTask(logger, "/tmp/myproject") - - // Execute the task if err := task.Run(context.Background()); err != nil { logger.Error("Task failed", "error", err) os.Exit(1) } - - logger.Info("Task completed successfully!") } ``` -## Features +## Core Concepts -- **Type-Safe Actions**: Generic-based architecture with compile-time safety -- **24+ Built-in Actions**: File operations, Docker management, system commands, package management -- **Action Parameter Passing**: Seamless data flow between actions and tasks using declarative parameter references -- **Lifecycle Management**: Before/Execute/After hooks with proper error handling -- **Context Support**: Cancellation and timeout handling -- **Comprehensive Logging**: Structured logging with configurable output -- **Easy Testing**: Built-in mocking support for all actions +- **Tasks**: Collections of actions that execute sequentially +- **Actions**: Individual operations (file, Docker, system) +- **Parameters**: Pass data between actions using `ActionOutput()` and `TaskOutput()` +- **Context**: Share data across tasks with `TaskManager` ## Built-in Actions -The Task Engine includes 19+ built-in actions for file operations, Docker management, system administration, and utilities. See [ACTIONS.md](ACTIONS.md) for complete documentation. +24+ actions for common operations: -```go -import "github.com/ndizazzo/task-engine/tasks" +- **File**: Create, read, write, copy, compress, extract +- **Docker**: Run, compose, health checks, image management +- **System**: Service management, package installation +- **Utilities**: Network info, prerequisites, timing -// Common task examples -dockerTask := tasks.NewDockerSetupTask(logger, "/path/to/project") -fileTask := tasks.NewFileOperationsTask(logger, "/path/to/project") -packageTask := tasks.NewPackageManagementTask(logger, []string{"git", "curl"}) -``` +See [ACTIONS.md](ACTIONS.md) for complete list. -## Action Parameter Passing +## Parameter Passing -Actions can reference outputs from previous actions and tasks using declarative parameters: +Actions can reference outputs from previous actions: ```go -// Action-to-action parameter passing +// Reference action output file.NewReplaceLinesAction( - "input.txt", + "config.txt", map[*regexp.Regexp]string{ - regexp.MustCompile("{{content}}"): - task_engine.ActionOutput("read-file", "content"), + regexp.MustCompile("{{version}}"): + task_engine.ActionOutput("read-file", "version"), }, logger, ) -// Cross-task parameter passing +// Reference task output docker.NewDockerRunAction( - task_engine.TaskOutput("build-app", "imageID"), + task_engine.TaskOutput("build", "imageID"), []string{"-p", "8080:8080"}, logger, ) ``` -See [docs/parameter_passing.md](docs/parameter_passing.md) for complete documentation. +## Task Management + +```go +manager := task_engine.NewTaskManager(logger) + +// Add and run tasks +taskID := manager.AddTask(task) +err := manager.RunTask(context.Background(), taskID) + +// Stop tasks +manager.StopTask(taskID) +manager.StopAllTasks() +``` -## Creating Custom Actions +## Custom Actions ```go type MyAction struct { @@ -100,13 +95,13 @@ type MyAction struct { } func (a *MyAction) Execute(ctx context.Context) error { - a.Logger.Info("Executing custom action", "message", a.Message) + a.Logger.Info("Executing", "message", a.Message) return nil } func NewMyAction(message string, logger *slog.Logger) *task_engine.Action[*MyAction] { return &task_engine.Action[*MyAction]{ - ID: "my-custom-action", + ID: "my-action", Wrapped: &MyAction{ BaseAction: task_engine.BaseAction{Logger: logger}, Message: message, @@ -115,50 +110,34 @@ func NewMyAction(message string, logger *slog.Logger) *task_engine.Action[*MyAct } ``` -## Task Management - -Use the `TaskManager` for advanced task orchestration: - -```go -manager := task_engine.NewTaskManager(logger) - -// Add and run tasks -taskID := manager.AddTask(task) -err := manager.RunTask(context.Background(), taskID) - -// Stop running tasks -manager.StopTask(taskID) -manager.StopAllTasks() -``` +## Examples -## Error Handling +See `tasks/` directory for complete examples: -Tasks automatically stop on first error and provide detailed error information: - -```go -if err := task.Run(ctx); err != nil { - if errors.Is(err, task_engine.ErrPrerequisiteNotMet) { - logger.Warn("Prerequisites not met, skipping task") - } else { - logger.Error("Task execution failed", "error", err) - } -} -``` +- File operations workflow +- Docker setup and management +- Package installation +- Archive extraction ## Testing -The module provides comprehensive testing support with mocks and performance testing: - ```go import "github.com/ndizazzo/task-engine/testing/mocks" -// Mock implementations mockManager := mocks.NewEnhancedTaskManagerMock() mockRunner := &mocks.MockCommandRunner{} ``` -See [testing/README.md](testing/README.md) for complete testing documentation. +## Documentation + +- [Quick Start](docs/QUICKSTART.md) - Get up and running in minutes +- [Architecture](docs/ARCHITECTURE.md) - Core concepts and structure +- [API Reference](docs/API.md) - Complete API documentation +- [Actions](ACTIONS.md) - Built-in actions reference +- [Examples](docs/examples/) - Usage examples and patterns +- [Testing](testing/README.md) - Testing utilities +- [Troubleshooting](docs/TROUBLESHOOTING.md) - Common issues and solutions ## License -This project is available under the MIT License. +MIT License diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 0000000..6a7e3cd --- /dev/null +++ b/docs/API.md @@ -0,0 +1,214 @@ +# API Reference + +## Core Types + +### Task + +```go +type Task struct { + ID string + RunID string + Name string + Actions []ActionWrapper + Logger *slog.Logger + TotalTime time.Duration + CompletedTasks int +} + +func (t *Task) Run(ctx context.Context) error +func (t *Task) RunWithContext(ctx context.Context, globalContext *GlobalContext) error +func (t *Task) GetID() string +func (t *Task) GetName() string +func (t *Task) GetCompletedTasks() int +func (t *Task) GetTotalTime() time.Duration +``` + +### Action + +```go +type Action[T ActionInterface] struct { + ID string + Wrapped T +} + +func (a *Action[T]) BeforeExecute(ctx context.Context) error +func (a *Action[T]) Execute(ctx context.Context) error +func (a *Action[T]) AfterExecute(ctx context.Context) error +func (a *Action[T]) GetOutput() interface{} +func (a *Action[T]) GetID() string +``` + +### ActionWrapper + +```go +type ActionWrapper func() ActionInterface +``` + +### TaskManager + +```go +type TaskManager struct { + // ... internal fields +} + +func NewTaskManager(logger *slog.Logger) *TaskManager +func (tm *TaskManager) AddTask(task *Task) error +func (tm *TaskManager) RunTask(ctx context.Context, taskID string) error +func (tm *TaskManager) StopTask(taskID string) error +func (tm *TaskManager) StopAllTasks() +func (tm *TaskManager) GetRunningTasks() []string +func (tm *TaskManager) IsTaskRunning(taskID string) bool +func (tm *TaskManager) GetGlobalContext() *GlobalContext +func (tm *TaskManager) ResetGlobalContext() +``` + +### GlobalContext + +```go +type GlobalContext struct { + ActionOutputs map[string]interface{} + TaskOutputs map[string]interface{} + ActionResults map[string]ResultProvider + mu sync.RWMutex +} + +func NewGlobalContext() *GlobalContext +func (gc *GlobalContext) SetActionOutput(actionID string, output interface{}) +func (gc *GlobalContext) SetTaskOutput(taskID string, output interface{}) +func (gc *GlobalContext) GetActionOutput(actionID string) (interface{}, bool) +func (gc *GlobalContext) GetTaskOutput(taskID string) (interface{}, bool) +``` + +## Parameter Types + +### StaticParameter + +```go +type StaticParameter struct { + Value interface{} +} + +func (p StaticParameter) Resolve(ctx context.Context, globalContext *GlobalContext) (interface{}, error) +``` + +### ActionOutputParameter + +```go +type ActionOutputParameter struct { + ActionID string + OutputKey string +} + +func (p ActionOutputParameter) Resolve(ctx context.Context, globalContext *GlobalContext) (interface{}, error) +``` + +### TaskOutputParameter + +```go +type TaskOutputParameter struct { + TaskID string + OutputKey string +} + +func (p TaskOutputParameter) Resolve(ctx context.Context, globalContext *GlobalContext) (interface{}, error) +``` + +### ActionResultParameter + +```go +type ActionResultParameter struct { + ActionID string + ResultKey string +} + +func (p ActionResultParameter) Resolve(ctx context.Context, globalContext *GlobalContext) (interface{}, error) +``` + +## Helper Functions + +### ActionOutput + +```go +func ActionOutput(actionID string, outputKey string) ActionOutputParameter +``` + +### TaskOutput + +```go +func TaskOutput(taskID string, outputKey string) TaskOutputParameter +``` + +### ActionResult + +```go +func ActionResult(actionID string, resultKey string) ActionResultParameter +``` + +## Interfaces + +### ActionInterface + +```go +type ActionInterface interface { + BeforeExecute(ctx context.Context) error + Execute(ctx context.Context) error + AfterExecute(ctx context.Context) error + GetOutput() interface{} +} +``` + +### TaskInterface + +```go +type TaskInterface interface { + GetID() string + GetName() string + Run(ctx context.Context) error + RunWithContext(ctx context.Context, globalContext *GlobalContext) error + GetCompletedTasks() int + GetTotalTime() time.Duration +} +``` + +### TaskManagerInterface + +```go +type TaskManagerInterface interface { + AddTask(task *Task) error + RunTask(taskID string) error + StopTask(taskID string) error + StopAllTasks() + GetRunningTasks() []string + IsTaskRunning(taskID string) bool + GetGlobalContext() *GlobalContext + ResetGlobalContext() +} +``` + +### ResultProvider + +```go +type ResultProvider interface { + GetResult() interface{} + GetError() error +} +``` + +## Constants + +```go +const GlobalContextKey contextKey = "globalContext" +``` + +## Errors + +```go +var ErrPrerequisiteNotMet = errors.New("task prerequisite not met") +``` + +## Context Keys + +```go +type contextKey string +const GlobalContextKey contextKey = "globalContext" +``` diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..0cb9426 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,97 @@ +# Architecture Overview + +## Core Components + +### Task + +A `Task` is a collection of `Action`s that execute sequentially. Tasks manage execution flow, error handling, and parameter resolution. + +```go +type Task struct { + ID string + Name string + Actions []ActionWrapper + Logger *slog.Logger +} +``` + +### Action + +An `Action` represents a single operation (file I/O, Docker command, system call). Actions implement the `ActionInterface` with Before/Execute/After lifecycle hooks. + +```go +type ActionInterface interface { + BeforeExecute(ctx context.Context) error + Execute(ctx context.Context) error + AfterExecute(ctx context.Context) error + GetOutput() interface{} +} +``` + +### ActionWrapper + +`ActionWrapper` is a function type that returns an action. This enables lazy initialization and parameter injection. + +```go +type ActionWrapper func() ActionInterface +``` + +### TaskManager + +`TaskManager` orchestrates multiple tasks, manages shared context, and provides task lifecycle control. + +## Parameter System + +### Static Parameters + +Fixed values known at task creation time. + +```go +engine.StaticParameter{Value: "/path/to/file"} +``` + +### Action Output Parameters + +Reference outputs from previous actions within the same task. + +```go +engine.ActionOutput("read-action", "content") +``` + +### Task Output Parameters + +Reference outputs from other tasks using the global context. + +```go +engine.TaskOutput("build-task", "imageID") +``` + +## Execution Flow + +1. **Task Creation**: Actions are wrapped in functions for lazy initialization +2. **Parameter Resolution**: Parameters are resolved at execution time using the global context +3. **Action Execution**: Each action runs Before → Execute → After hooks +4. **Output Storage**: Action outputs are stored in the global context for parameter passing +5. **Error Handling**: Tasks stop on first error, with special handling for prerequisites + +## Context Management + +The `GlobalContext` maintains: + +- `ActionOutputs`: Results from completed actions +- `TaskOutputs`: Results from completed tasks +- `ActionResults`: Rich results from actions implementing `ResultProvider` + +Context is shared across tasks via the `TaskManager` and embedded in the execution context. + +## Error Handling + +- **Prerequisites**: Return `ErrPrerequisiteNotMet` to gracefully abort tasks +- **Execution Errors**: Stop task execution and return error details +- **Context Cancellation**: Respect context cancellation for timeouts and graceful shutdown + +## Testing Support + +- **Mocks**: Complete mock implementations for all interfaces +- **Testable Manager**: Enhanced TaskManager with testing hooks +- **Performance Testing**: Built-in benchmarking and load testing utilities diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md new file mode 100644 index 0000000..d778fbf --- /dev/null +++ b/docs/QUICKSTART.md @@ -0,0 +1,222 @@ +# Quick Start Guide + +Get up and running with Task Engine in minutes. + +## Installation + +```bash +go get github.com/ndizazzo/task-engine +``` + +## Basic Task + +Create a simple task that creates a directory and writes a file: + +```go +package main + +import ( + "context" + "log/slog" + "os" + + "github.com/ndizazzo/task-engine" + "github.com/ndizazzo/task-engine/actions/file" +) + +func main() { + logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) + + // Create task with two actions + task := &task_engine.Task{ + ID: "my-first-task", + Name: "Create Project Structure", + Actions: []task_engine.ActionWrapper{ + // Action 1: Create directory + func() task_engine.ActionInterface { + action, _ := file.NewCreateDirectoriesAction(logger).WithParameters( + task_engine.StaticParameter{Value: "/tmp/myproject"}, + task_engine.StaticParameter{Value: []string{"src", "docs"}}, + ) + return action + }, + + // Action 2: Write file + func() task_engine.ActionInterface { + action, _ := file.NewWriteFileAction(logger).WithParameters( + task_engine.StaticParameter{Value: "/tmp/myproject/README.md"}, + task_engine.StaticParameter{Value: []byte("# My Project\n\nCreated with Task Engine!")}, + true, // overwrite + nil, // inputBuffer + ) + return action + }, + }, + Logger: logger, + } + + // Run the task + if err := task.Run(context.Background()); err != nil { + logger.Error("Task failed", "error", err) + os.Exit(1) + } + + logger.Info("Task completed successfully!") +} +``` + +## Using Built-in Examples + +Task Engine provides ready-to-use examples: + +```go +import "github.com/ndizazzo/task-engine/tasks" + +// File operations workflow +fileTask := tasks.NewFileOperationsTask(logger, "/tmp/project") + +// Docker setup +dockerTask := tasks.NewDockerSetupTask(logger, "/path/to/compose") + +// Package installation +packageTask := tasks.NewPackageManagementTask(logger, []string{"git", "curl"}) + +// Run any task +if err := fileTask.Run(context.Background()); err != nil { + logger.Error("Task failed", "error", err) +} +``` + +## Parameter Passing + +Pass data between actions: + +```go +task := &task_engine.Task{ + ID: "file-pipeline", + Name: "Process File", + Actions: []task_engine.ActionWrapper{ + // Read file + func() task_engine.ActionInterface { + var content []byte + action, _ := file.NewReadFileAction("/tmp/input.txt", &content, logger) + action.ID = "read-file" + return action + }, + + // Process content (using output from read action) + func() task_engine.ActionInterface { + action := file.NewReplaceLinesAction(logger).WithParameters( + task_engine.StaticParameter{Value: "/tmp/output.txt"}, + map[*regexp.Regexp]task_engine.ActionParameter{ + regexp.MustCompile("old"): task_engine.ActionOutput("read-file", "content"), + }, + ) + return action + }, + }, + Logger: logger, +} +``` + +## Task Manager + +Manage multiple tasks with shared context: + +```go +manager := task_engine.NewTaskManager(logger) + +// Add tasks +task1ID := manager.AddTask(fileTask) +task2ID := manager.AddTask(dockerTask) + +// Run tasks +if err := manager.RunTask(context.Background(), task1ID); err != nil { + logger.Error("Task 1 failed", "error", err) +} + +if err := manager.RunTask(context.Background(), task2ID); err != nil { + logger.Error("Task 2 failed", "error", err) +} + +// Stop tasks +manager.StopTask(task1ID) +manager.StopAllTasks() +``` + +## Custom Actions + +Create your own actions: + +```go +type GreetingAction struct { + task_engine.BaseAction + Name string +} + +func (a *GreetingAction) Execute(ctx context.Context) error { + a.Logger.Info("Hello", "name", a.Name) + return nil +} + +func NewGreetingAction(name string, logger *slog.Logger) *task_engine.Action[*GreetingAction] { + return &task_engine.Action[*GreetingAction]{ + ID: "greeting", + Wrapped: &GreetingAction{ + BaseAction: task_engine.BaseAction{Logger: logger}, + Name: name, + }, + } +} + +// Use in task +greetingAction := NewGreetingAction("World", logger) +task.Actions = append(task.Actions, func() task_engine.ActionInterface { + return greetingAction +}) +``` + +## Error Handling + +Handle errors gracefully: + +```go +if err := task.Run(ctx); err != nil { + if errors.Is(err, task_engine.ErrPrerequisiteNotMet) { + logger.Warn("Prerequisites not met, skipping task") + return nil + } + logger.Error("Task execution failed", "error", err) + return err +} +``` + +## Testing + +Test your tasks with built-in utilities: + +```go +import "github.com/ndizazzo/task-engine/testing/mocks" + +func TestMyTask(t *testing.T) { + // Create mock logger + logger := slog.New(slog.NewTextHandler(io.Discard, nil)) + + // Create mock task manager + mockManager := mocks.NewEnhancedTaskManagerMock() + + // Test your task + task := createMyTask(logger) + err := task.Run(context.Background()) + + assert.NoError(t, err) +} +``` + +## Next Steps + +- Explore [built-in actions](ACTIONS.md) +- Check [examples](docs/examples/) for patterns +- Read [architecture overview](docs/ARCHITECTURE.md) +- Review [API reference](docs/API.md) +- See [troubleshooting](docs/TROUBLESHOOTING.md) for common issues diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md new file mode 100644 index 0000000..3bcc137 --- /dev/null +++ b/docs/TROUBLESHOOTING.md @@ -0,0 +1,237 @@ +# Troubleshooting + +Common issues and solutions for the Task Engine. + +## Parameter Resolution Errors + +### "action not found in context" + +**Problem**: Action output referenced before action executes. + +```go +// ❌ Wrong - action doesn't exist yet +engine.ActionOutput("read-file", "content") + +// ✅ Correct - reference existing action +engine.ActionOutput("read-source", "content") +``` + +**Solution**: Ensure action IDs match exactly and actions execute before being referenced. + +### "output key not found" + +**Problem**: Referenced output key doesn't exist in action output. + +```go +// ❌ Wrong - key doesn't exist +engine.ActionOutput("read-file", "nonexistent") + +// ✅ Correct - use existing key or omit for entire output +engine.ActionOutput("read-file", "content") +engine.ActionOutput("read-file", "") // entire output +``` + +**Solution**: Check action output structure or omit key for entire output. + +## Task Execution Issues + +### Task stops on first error + +**Problem**: Task halts when any action fails. + +```go +// ❌ Task stops here +action1.Execute(ctx) // fails +action2.Execute(ctx) // never runs + +// ✅ Handle errors gracefully +if err := action1.Execute(ctx); err != nil { + if errors.Is(err, engine.ErrPrerequisiteNotMet) { + // Skip task gracefully + return nil + } + return err +} +``` + +**Solution**: Use `ErrPrerequisiteNotMet` for graceful task abortion. + +### Context cancellation + +**Problem**: Task doesn't respect context cancellation. + +```go +// ✅ Always check context in long-running actions +func (a *MyAction) Execute(ctx context.Context) error { + for { + select { + case <-ctx.Done(): + return ctx.Err() + default: + // Do work + } + } +} +``` + +**Solution**: Check `ctx.Done()` in long-running operations. + +## Docker Issues + +### Container not found + +**Problem**: Container name/ID doesn't exist. + +```go +// ❌ Container might not exist +docker.NewGetContainerStateAction(logger, "my-container") + +// ✅ Check if container exists first +containers := docker.NewGetAllContainersStateAction(logger) +// Filter by name/ID +``` + +**Solution**: Use `GetAllContainersStateAction` to verify container existence. + +### Permission denied + +**Problem**: Docker commands fail due to permissions. + +```go +// ❌ Might fail without proper permissions +docker.NewDockerRunAction(logger, "nginx", nil, nil, nil) + +// ✅ Ensure user is in docker group +// Run: sudo usermod -aG docker $USER +``` + +**Solution**: Add user to docker group or use sudo. + +## File Operation Issues + +### Path not found + +**Problem**: File/directory doesn't exist. + +```go +// ❌ Path might not exist +file.NewReadFileAction("/nonexistent/file", &content, logger) + +// ✅ Create directories first +file.NewCreateDirectoriesAction(logger).WithParameters( + engine.StaticParameter{Value: "/path/to"}, + engine.StaticParameter{Value: []string{"parent", "child"}}, +) +``` + +**Solution**: Create parent directories before file operations. + +### Permission denied + +**Problem**: Insufficient file permissions. + +```go +// ❌ Might fail without proper permissions +file.NewWriteFileAction("/etc/config", content, true, nil, logger) + +// ✅ Check permissions or use appropriate paths +file.NewWriteFileAction("/tmp/config", content, true, nil, logger) +``` + +**Solution**: Use appropriate paths or check file permissions. + +## System Service Issues + +### Service not found + +**Problem**: Systemd service doesn't exist. + +```go +// ❌ Service might not exist +system.NewGetServiceStatusAction(logger, "nonexistent-service") + +// ✅ Check service existence first +// Use systemctl list-units --type=service +``` + +**Solution**: Verify service name with `systemctl list-units`. + +### Service management fails + +**Problem**: Insufficient privileges for service control. + +```go +// ❌ Might fail without sudo +system.NewManageServiceAction(logger).WithParameters( + engine.StaticParameter{Value: "nginx"}, + engine.StaticParameter{Value: "restart"}, +) + +// ✅ Ensure proper privileges or use sudo +``` + +**Solution**: Run with appropriate privileges or use sudo. + +## Common Patterns + +### Error Handling + +```go +// ✅ Proper error handling +if err := task.Run(ctx); err != nil { + if errors.Is(err, engine.ErrPrerequisiteNotMet) { + logger.Warn("Prerequisites not met", "error", err) + return nil + } + logger.Error("Task failed", "error", err) + return err +} +``` + +### Parameter Validation + +```go +// ✅ Validate parameters before use +if actionID == "" { + return fmt.Errorf("action ID cannot be empty") +} +``` + +### Context Usage + +```go +// ✅ Always pass context through +func (a *MyAction) Execute(ctx context.Context) error { + // Use ctx for cancellation, timeouts, etc. + return nil +} +``` + +## Debugging + +### Enable Debug Logging + +```go +logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ + Level: slog.LevelDebug, +})) +``` + +### Check Action Outputs + +```go +// Print all action outputs +for id, output := range globalContext.ActionOutputs { + logger.Debug("Action output", "id", id, "output", output) +} +``` + +### Verify Task State + +```go +// Check task execution state +logger.Info("Task state", + "completed", task.GetCompletedTasks(), + "totalTime", task.GetTotalTime(), +) +``` diff --git a/docs/action_update_prompt.md b/docs/action_update_prompt.md deleted file mode 100644 index 4c98cfa..0000000 --- a/docs/action_update_prompt.md +++ /dev/null @@ -1,48 +0,0 @@ -# Action Update Prompt - -You must update all actions in the task-engine codebase to follow the single builder pattern documented in docs/passing.md. Apply these rules strictly: - -## Required Implementation Pattern - -1. **Single Constructor**: Only implement NewActionName(logger *slog.Logger) *ActionName - returns the action struct directly -2. **Single Action Struct**: ActionName struct contains BaseAction and parameter fields - no separate builder struct needed -3. **WithParameters Method**: Method on action struct that returns (*task_engine.Action[*ActionName], error) - always include error return -4. **Parameter Validation**: Check parameters are not nil in WithParameters, return descriptive errors -5. **Parameter-Only Action Struct**: Action struct contains ONLY task_engine.BaseAction, ActionParameter fields, and execution result fields (for GetOutput) - no static input fields - - Input fields must be ActionParameter types (resolved at runtime) - - Result fields (string, int, etc.) are allowed for storing execution results returned by GetOutput() -6. **Runtime Resolution**: All parameters resolved in Execute() method using globalContext -7. **Type Checking**: Validate resolved parameter types with meaningful error messages -8. **Error Wrapping**: Wrap parameter resolution errors with context using fmt.Errorf - -## Prohibited Patterns - -1. **No Legacy Constructors**: Remove all constructors that take static parameters directly -2. **No Static Input Fields**: Action structs cannot have string, int, []string or other static input value fields - use ActionParameter instead. Result fields for GetOutput() are allowed. -3. **No Builder Suffix**: Constructor names must be NewActionName, not NewActionNameBuilder -4. **No Separate Builder Structs**: Use single action struct with WithParameters method, no separate builder struct needed -5. **No WithParameters Without Error**: WithParameters must always return error as second value -6. **No Dual-Mode Structs**: No mixing static fields with parameter fields -7. **No Custom Builder Methods**: No WithDescription, WithTimeout, etc - only WithParameters -8. **No Helper Constructors**: Do not add NewActionNameWithStatic or similar convenience functions - -## Test Update Requirements - -1. **Use Builder Pattern**: Replace all direct constructor calls with NewActionName(logger).WithParameters(params) -2. **Handle Errors**: Always use suite.Require().NoError(err) after WithParameters calls -3. **Parameter Types**: Wrap all test values in task_engine.StaticParameter{Value: value} -4. **Error Variable Names**: Use execErr for Execute() errors to avoid shadowing -5. **GlobalContext Testing**: Include tests with ActionOutputParameter to verify parameter resolution - -## Implementation Steps - -1. Update action struct to remove all static fields, keep only ActionParameter fields -2. Update builder struct to contain only logger field -3. Remove Builder suffix from constructor function name -4. Add error return to WithParameters method with nil parameter validation -5. Update Execute method to resolve all parameters at runtime using local variables -6. Remove any legacy constructors or helper functions -7. Update all test files to use new pattern with proper error handling -8. Run tests after each action update to ensure correctness - -Apply these changes systematically to each action, updating both the action file and its corresponding test file before moving to the next action. Always verify tests pass after each update. diff --git a/docs/examples/README.md b/docs/examples/README.md index 12d9713..3afda72 100644 --- a/docs/examples/README.md +++ b/docs/examples/README.md @@ -2,6 +2,29 @@ Working examples for the Task Engine. +## Quick Examples + +### File Operations + +```go +task := tasks.NewFileOperationsTask(logger, "/tmp/project") +err := task.Run(context.Background()) +``` + +### Docker Setup + +```go +task := tasks.NewDockerSetupTask(logger, "/path/to/compose") +err := task.Run(context.Background()) +``` + +### Package Management + +```go +task := tasks.NewPackageManagementTask(logger, []string{"git", "curl"}) +err := task.Run(context.Background()) +``` + ## Parameter Passing [parameter_passing_examples.md](parameter_passing_examples.md) - Complete examples of action parameter passing: diff --git a/docs/features/0001_PLAN.md b/docs/features/0001_PLAN.md deleted file mode 100644 index 5cd588c..0000000 --- a/docs/features/0001_PLAN.md +++ /dev/null @@ -1,431 +0,0 @@ -# Task Manager Testability Improvements - -## Overview - -This document outlines the plan to improve the testability of the task-engine library. The current implementation has several testing challenges that limit downstream clients' ability to write effective unit tests. - -## Current Issues - -### 1. **Concrete Type Dependency** - -- Downstream clients expect concrete `*task_engine.TaskManager` types -- Makes it impossible to use mocks in tests -- Forces tests to use real task manager instances - -### 2. **Limited Interface** - -- The external `task-engine` module doesn't provide a proper interface for mocking -- No abstraction layer between business logic and task execution -- Tight coupling between components - -### 3. **Stateful Operations** - -- Task manager maintains internal state that's difficult to control in tests -- No way to reset state between test runs -- Hard to verify internal state changes - -### 4. **No Result Retrieval (address this later)** - -- Difficult to assert expected outcomes in tests -- No standardized way to handle task results -- Tasks don't provide a clean way to retrieve results for verification (this will be adressed in another update) - -The goal is to provide a nice way for downstream clients to more easily write unit tests for their applications implementing task-engine. - -## Recommended Improvements - -### 1. Create Interface Wrapper - -```go -// interface.go -package task_engine - -import ( - "context" - "log/slog" -) - -// TaskManagerInterface defines the contract for task management -type TaskManagerInterface interface { - AddTask(task *Task) error - RunTask(taskID string) error - StopTask(taskID string) error - StopAllTasks() - GetRunningTasks() []string - IsTaskRunning(taskID string) bool -} - -// TaskInterface defines the contract for individual tasks -type TaskInterface interface { - GetID() string - GetName() string - Run(ctx context.Context) error - GetCompletedTasks() int - GetTotalTime() time.Duration -} - -// ResultProvider interface for tasks that produce results -type ResultProvider interface { - GetResult() interface{} - GetError() error -} -``` - -### 2. Implement a Testable Task Manager - -```go -// testable_manager.go -package task_engine - -import ( - "log/slog" - "sync" - "time" -) - -// TestableTaskManager provides enhanced testing capabilities -type TestableTaskManager struct { - *TaskManager - mu sync.RWMutex - - // Testing hooks - onTaskAdded func(*Task) - onTaskStarted func(string) - onTaskCompleted func(string, error) - - // Result storage for testing - taskResults map[string]interface{} - taskErrors map[string]error -} - -// NewTestableTaskManager creates a testable task manager -func NewTestableTaskManager(logger *slog.Logger) *TestableTaskManager { - return &TestableTaskManager{ - TaskManager: NewTaskManager(logger), - taskResults: make(map[string]interface{}), - taskErrors: make(map[string]error), - } -} - -// SetTaskAddedHook sets a callback for when tasks are added -func (tm *TestableTaskManager) SetTaskAddedHook(hook func(*Task)) { - tm.mu.Lock() - defer tm.mu.Unlock() - tm.onTaskAdded = hook -} - -// SetTaskStartedHook sets a callback for when tasks start -func (tm *TestableTaskManager) SetTaskStartedHook(hook func(string)) { - tm.mu.Lock() - defer tm.mu.Unlock() - tm.onTaskStarted = hook -} - -// SetTaskCompletedHook sets a callback for when tasks complete -func (tm *TestableTaskManager) SetTaskCompletedHook(hook func(string, error)) { - tm.mu.Lock() - defer tm.mu.Unlock() - tm.onTaskCompleted = hook -} - -// OverrideTaskResult allows tests to set expected results -func (tm *TestableTaskManager) OverrideTaskResult(taskID string, result interface{}) { - tm.mu.Lock() - defer tm.mu.Unlock() - tm.taskResults[taskID] = result -} - -// OverrideTaskError allows tests to set expected errors -func (tm *TestableTaskManager) OverrideTaskError(taskID string, err error) { - tm.mu.Lock() - defer tm.mu.Unlock() - tm.taskErrors[taskID] = err -} - -// GetTaskResult retrieves the result for a specific task -func (tm *TestableTaskManager) GetTaskResult(taskID string) (interface{}, bool) { - tm.mu.RLock() - defer tm.mu.RUnlock() - result, exists := tm.taskResults[taskID] - return result, exists -} - -// GetTaskError retrieves the error for a specific task -func (tm *TestableTaskManager) GetTaskError(taskID string) (error, bool) { - tm.mu.RLock() - defer tm.mu.RUnlock() - err, exists := tm.taskErrors[taskID] - return err, exists -} - -// ClearTestData clears all test-related data -func (tm *TestableTaskManager) ClearTestData() { - tm.mu.Lock() - defer tm.mu.Unlock() - tm.taskResults = make(map[string]interface{}) - tm.taskErrors = make(map[string]error) -} -``` - -### 3. Enhanced Mock Implementation - -```go -// mocks/task_manager_mock.go -package mocks - -import ( - "sync" - "github.com/stretchr/testify/mock" - task_engine "github.com/ndizazzo/task-engine" -) - -// EnhancedTaskManagerMock provides comprehensive mocking capabilities -type EnhancedTaskManagerMock struct { - mock.Mock - mu sync.RWMutex - - // State tracking - tasks map[string]*task_engine.Task - runningTasks map[string]bool - taskResults map[string]interface{} - taskErrors map[string]error - - // Call tracking - addTaskCalls []*task_engine.Task - runTaskCalls []string - stopTaskCalls []string -} - -// NewEnhancedTaskManagerMock creates a new enhanced mock -func NewEnhancedTaskManagerMock() *EnhancedTaskManagerMock { - return &EnhancedTaskManagerMock{ - tasks: make(map[string]*task_engine.Task), - runningTasks: make(map[string]bool), - taskResults: make(map[string]interface{}), - taskErrors: make(map[string]error), - } -} - -// AddTask mocks AddTask with state tracking -func (m *EnhancedTaskManagerMock) AddTask(task *task_engine.Task) error { - args := m.Called(task) - - m.mu.Lock() - defer m.mu.Unlock() - - if task != nil { - m.tasks[task.ID] = task - m.addTaskCalls = append(m.addTaskCalls, task) - } - - return args.Error(0) -} - -// RunTask mocks RunTask with state tracking -func (m *EnhancedTaskManagerMock) RunTask(taskID string) error { - args := m.Called(taskID) - - m.mu.Lock() - defer m.mu.Unlock() - - m.runningTasks[taskID] = true - m.runTaskCalls = append(m.runTaskCalls, taskID) - - return args.Error(0) -} - -// StopTask mocks StopTask with state tracking -func (m *EnhancedTaskManagerMock) StopTask(taskID string) error { - args := m.Called(taskID) - - m.mu.Lock() - defer m.mu.Unlock() - - delete(m.runningTasks, taskID) - m.stopTaskCalls = append(m.stopTaskCalls, taskID) - - return args.Error(0) -} - -// StopAllTasks mocks StopAllTasks -func (m *EnhancedTaskManagerMock) StopAllTasks() { - m.Called() - - m.mu.Lock() - defer m.mu.Unlock() - - m.runningTasks = make(map[string]bool) -} - -// GetRunningTasks returns the current running tasks -func (m *EnhancedTaskManagerMock) GetRunningTasks() []string { - args := m.Called() - - m.mu.RLock() - defer m.mu.RUnlock() - - var running []string - for taskID, running := range m.runningTasks { - if running { - running = append(running, taskID) - } - } - - if args.Get(0) != nil { - return args.Get(0).([]string) - } - return running -} - -// IsTaskRunning checks if a specific task is running -func (m *EnhancedTaskManagerMock) IsTaskRunning(taskID string) bool { - args := m.Called(taskID) - - m.mu.RLock() - defer m.mu.RUnlock() - - if args.Get(0) != nil { - return args.Bool(0) - } - return m.runningTasks[taskID] -} - -// SetTaskResult allows tests to set expected results -func (m *EnhancedTaskManagerMock) SetTaskResult(taskID string, result interface{}) { - m.mu.Lock() - defer m.mu.Unlock() - m.taskResults[taskID] = result -} - -// SetTaskError allows tests to set expected errors -func (m *EnhancedTaskManagerMock) SetTaskError(taskID string, err error) { - m.mu.Lock() - defer m.mu.Unlock() - m.taskErrors[taskID] = err -} - -// GetAddedTasks returns all tasks that were added -func (m *EnhancedTaskManagerMock) GetAddedTasks() []*task_engine.Task { - m.mu.RLock() - defer m.mu.RUnlock() - return append([]*task_engine.Task{}, m.addTaskCalls...) -} - -// GetRunTaskCalls returns all RunTask calls -func (m *EnhancedTaskManagerMock) GetRunTaskCalls() []string { - m.mu.RLock() - defer m.mu.RUnlock() - return append([]string{}, m.runTaskCalls...) -} - -// ClearHistory clears all call history -func (m *EnhancedTaskManagerMock) ClearHistory() { - m.mu.Lock() - defer m.mu.Unlock() - m.addTaskCalls = nil - m.runTaskCalls = nil - m.stopTaskCalls = nil -} -``` - -### 4. Example Usage in Downstream Projects - -```go -// Example: How downstream projects can use the interface -type UpdateManager struct { - // ... other fields ... - taskManager task_engine.TaskManagerInterface // Use interface instead of concrete type - // ... other fields ... -} - -// Constructor accepts interface -func NewUpdateManager( - config Config, - display display.Manager, - taskManager task_engine.TaskManagerInterface, // Accept interface - logger *slog.Logger, -) *UpdateManager { - // ... implementation ... -} -``` - -## Benefits of These Improvements - -### 1. **Easier Mocking** - -- Tests can use the enhanced mock without type conflicts -- No more concrete type dependency issues -- Clean separation between test and production code - -### 2. **Better State Control** - -- Tests can set expected results and errors -- Predictable behavior in test scenarios -- Easy to test edge cases and error conditions - -### 3. **Call Verification** - -- Tests can verify exactly what methods were called with what arguments -- Comprehensive assertion capabilities -- Better debugging when tests fail - -### 4. **Test Isolation** - -- Tests can run in isolation without affecting each other -- No shared state between test runs -- Consistent test behavior - -### 5. **Flexibility** - -- Tests can choose between real implementation and mocks -- Easy to switch between different testing strategies -- Support for both unit and integration tests - -### 6. **Maintainability** - -- Interface-based design makes the code more maintainable -- Easier to refactor and modify -- Better adherence to SOLID principles - -## Implementation Priority - -### **High Priority (Immediate)** - -1. Create the interface wrapper (`internal/task_engine/interface.go`) -2. Update existing tests to use the new interface - -### **Medium Priority** - -1. Implement the enhanced mock (`tests/mocks/task_manager_mock.go`) -2. Add comprehensive test coverage for task manager interactions - -### **Low Priority** - -1. Implement the testable task manager (`internal/task_engine/testable_manager.go`) -2. Add advanced testing features like hooks and callbacks -3. Performance testing and benchmarking capabilities - -## Migration Strategy - -### Phase 1: Interface Introduction - -- Create interfaces without breaking existing code -- Migrate components to use interfaces - -### Phase 2: Mock Implementation - -- Implement enhanced mocks -- Update existing tests to use new mocks -- Remove dependency on concrete types in tests - -### Phase 3: Advanced Features - -- Add testable task manager implementation -- Implement advanced testing capabilities -- Performance optimization and benchmarking - -## Conclusion - -These improvements will significantly enhance the testability of the task manager while maintaining backward compatibility and improving the overall architecture. The interface-based approach will make the code more maintainable and easier to test, leading to higher quality and more reliable software. - -The phased implementation approach ensures that we can gradually migrate without disrupting existing functionality, while the enhanced mocking capabilities will make our tests more robust and easier to maintain. diff --git a/docs/features/0001_REVIEW.md b/docs/features/0001_REVIEW.md deleted file mode 100644 index 76121b8..0000000 --- a/docs/features/0001_REVIEW.md +++ /dev/null @@ -1,253 +0,0 @@ -# Code Review: Task Manager Testability Improvements - -## Overview - -This document provides a thorough code review of the implemented feature described in `0001_PLAN.md`. The implementation successfully addresses the testability challenges outlined in the plan and provides a solid foundation for downstream clients to write effective unit tests. - -## ✅ Plan Implementation Assessment - -### 1. Interface Wrapper - **FULLY IMPLEMENTED** - -The plan called for creating interfaces to abstract task management functionality. This has been **completely implemented** in `interface.go`: - -- ✅ `TaskManagerInterface` - Defines the contract for task management -- ✅ `TaskInterface` - Defines the contract for individual tasks -- ✅ `ResultProvider` - Interface for tasks that produce results - -**Implementation Quality**: The interfaces are clean, well-defined, and follow Go best practices. They provide the exact abstraction layer needed for mocking. - -### 2. Testable Task Manager - **FULLY IMPLEMENTED** - -The plan outlined a `TestableTaskManager` with enhanced testing capabilities. This has been **completely implemented** in `testable_manager.go`: - -- ✅ Testing hooks for task lifecycle events -- ✅ Result and error override capabilities -- ✅ Call tracking for verification -- ✅ State management and cleanup methods -- ✅ Thread-safe operations with proper mutex usage - -**Implementation Quality**: The implementation exceeds the plan's requirements with additional features like timing overrides and comprehensive metrics. - -### 3. Enhanced Mock Implementation - **FULLY IMPLEMENTED** - -The plan described an enhanced mock with comprehensive capabilities. This has been **completely implemented** in `mocks/task_manager_mock.go`: - -- ✅ State tracking for tasks and running states -- ✅ Call history tracking -- ✅ Result and error simulation -- ✅ Comprehensive verification methods -- ✅ Clean state management - -**Implementation Quality**: The mock implementation is robust and provides extensive testing capabilities beyond what was outlined in the plan. - -## 🔍 Code Quality Analysis - -### Strengths - -1. **Comprehensive Test Coverage**: All new components have thorough test coverage -2. **Thread Safety**: Proper use of mutexes for concurrent access -3. **Clean Architecture**: Clear separation of concerns between interfaces and implementations -4. **Backward Compatibility**: Existing code continues to work without changes -5. **Extensive Mocking**: Rich set of methods for test verification and state management - -### Code Structure and Style - -1. **Consistent Naming**: Follows Go conventions and matches existing codebase style -2. **Proper Error Handling**: Consistent error handling patterns throughout -3. **Documentation**: Good inline documentation and clear method names -4. **Interface Design**: Well-designed interfaces that follow Go interface design principles - -## 🐛 Issues and Concerns - -### 1. **Minor Issue: Missing Interface Implementation Check** - -In `interface_test.go`, there's a potential issue: - -```go -// TestTaskManagerImplementsInterface verifies that TaskManager implements TaskManagerInterface -func (suite *InterfaceTestSuite) TestTaskManagerImplementsInterface() { - var _ TaskManagerInterface = (*TaskManager)(nil) -} -``` - -This test only checks compile-time interface compliance but doesn't verify runtime behavior. Consider adding runtime verification tests. - -### 2. **Potential Issue: Mutex Locking in TestableTaskManager** - -In `testable_manager.go`, the hook execution pattern could potentially cause issues: - -```go -// Execute hook if set (outside of lock to avoid deadlocks) -if hook != nil { - hook(task) -} -``` - -While this avoids deadlocks, it means hooks could execute with stale data. Consider documenting this behavior or providing a safer alternative. - -### 3. **Minor Issue: Mock State Consistency** - -The mock's `VerifyAllExpectations` method has a potential issue: - -```go -func (m *EnhancedTaskManagerMock) VerifyAllExpectations() map[string]bool { - // ... - allExpectationsMet := true - for _, expectedCall := range m.ExpectedCalls { - if expectedCall.Repeatability > 0 { - allExpectationsMet = false - break - } - } - // ... -} -``` - -This logic might not correctly verify all expectations. Consider using testify's built-in verification methods. - -## 🔧 Recommendations for Improvement - -### 1. **Add Runtime Interface Verification** - -```go -func TestTaskManagerRuntimeInterfaceCompliance(t *testing.T) { - logger := slog.Default() - tm := NewTaskManager(logger) - - // Test that all interface methods work as expected - task := &Task{ID: "test", Name: "Test", Actions: []ActionWrapper{}} - - err := tm.AddTask(task) - assert.NoError(t, err) - - err = tm.RunTask("test") - assert.NoError(t, err) - - // ... test other interface methods -} -``` - -### 2. **Improve Hook Safety in TestableTaskManager** - -Consider adding a method to safely execute hooks with proper data copying: - -```go -func (tm *TestableTaskManager) executeHookSafely(hook func(*Task), task *Task) { - if hook != nil { - // Create a copy of the task to avoid race conditions - taskCopy := *task - hook(&taskCopy) - } -} -``` - -### 3. **Enhance Mock Verification** - -Improve the mock's verification capabilities: - -```go -func (m *EnhancedTaskManagerMock) VerifyAllExpectations() error { - // Use testify's built-in verification - if !m.AssertExpectations(mock.Anything) { - return errors.New("not all expectations were met") - } - return nil -} -``` - -## 📊 Performance Considerations - -### 1. **Memory Usage** - -The `TestableTaskManager` maintains additional maps and slices for testing purposes. This is acceptable for testing scenarios but should be documented. - -### 2. **Lock Contention** - -The extensive use of mutexes in the testable manager could impact performance in high-concurrency scenarios. However, this is primarily intended for testing, so the performance impact is acceptable. - -## 🔒 Security Considerations - -No security issues identified. The implementation follows secure coding practices: - -- Proper input validation -- No exposure of internal state -- Thread-safe operations -- Clean separation of concerns - -## 📈 Scalability Assessment - -### 1. **Interface Design** - -The interface-based approach makes the system highly scalable: - -- Easy to add new implementations -- Simple to extend with new methods -- Clean dependency injection support - -### 2. **Mock Capabilities** - -The enhanced mock provides excellent scalability for testing: - -- Supports complex test scenarios -- Easy to extend with new verification methods -- Maintains state consistency across test runs - -## 🧪 Testing Quality - -### 1. **Test Coverage** - -- **Interface Tests**: ✅ Complete -- **TestableManager Tests**: ✅ Comprehensive -- **Mock Tests**: ✅ Thorough -- **Integration Tests**: ✅ Existing tests still pass - -### 2. **Test Patterns** - -The tests follow excellent patterns: - -- Table-driven tests where appropriate -- Proper setup and teardown -- Clear test names and descriptions -- Good use of assertions and requirements - -## 📋 Migration Impact - -### 1. **Backward Compatibility** - -✅ **FULLY MAINTAINED** - No breaking changes introduced - -### 2. **Existing Code** - -✅ **NO CHANGES REQUIRED** - All existing code continues to work - -### 3. **Downstream Impact** - -✅ **POSITIVE** - Downstream clients can now easily implement mocking - -## 🎯 Conclusion - -### Overall Assessment: **EXCELLENT** ✅ - -The implementation successfully addresses all the requirements outlined in the plan and exceeds expectations in several areas: - -1. **Plan Compliance**: 100% - All planned features implemented -2. **Code Quality**: High - Clean, well-tested, maintainable code -3. **Architecture**: Excellent - Proper interface design and separation of concerns -4. **Testing**: Comprehensive - Thorough test coverage for all new components -5. **Documentation**: Good - Clear code structure and inline documentation - -### Key Achievements - -- ✅ **Interface-based design** eliminates concrete type dependencies -- ✅ **Enhanced mocking capabilities** provide comprehensive testing support -- ✅ **Testable task manager** offers advanced testing features -- ✅ **Backward compatibility** maintained throughout -- ✅ **Thread-safe operations** ensure reliability in concurrent scenarios - -### Recommendations - -1. **Immediate**: Address the minor issues identified above -2. **Short-term**: Add runtime interface verification tests -3. **Long-term**: Consider adding performance benchmarks for the testable manager - -This implementation significantly improves the testability of the task-engine library while maintaining high code quality and following Go best practices. Downstream clients will now be able to write much more effective unit tests with minimal effort. diff --git a/docs/features/0002_PLAN.md b/docs/features/0002_PLAN.md deleted file mode 100644 index 9ecbaa5..0000000 --- a/docs/features/0002_PLAN.md +++ /dev/null @@ -1,1147 +0,0 @@ -# Feature 0002: Action Parameter Passing - -## Problem Statement - -The current task engine architecture has a fundamental limitation: actions cannot easily pass data to subsequent actions. This creates several issues: - -1. **Complex Action Coupling**: Actions must be aware of previous action results, making them less reusable -2. **Runtime Data Access**: Actions try to access data from other actions at creation time, before execution - -### Current Problem Example - -```go -// Current problematic approach -func NewCleanupAgentsTask(config CleanupAgentsTaskConfig, logger *slog.Logger) *task_engine.Task { - listAction := agent_actions.NewListAgentFilesAction(config.AgentDirectory, logger) - - // This fails - AgentFiles is empty at creation time - var paths []string - for _, item := range listAction.Wrapped.AgentFiles { - paths = append(paths, item.Path) - } - - removeAction := file.NewRemoveFilesAction(paths, logger) - // ... -} -``` - -## Solution Overview - -The updated task engine will support declarative parameter passing between actions **and tasks** while maintaining clean, reusable definitions. Each action and task can define its own parameter type, and the task engine handles all parameter mapping at execution time. - -### Scope: Cross-Entity Parameter Passing - -This system enables comprehensive data flow across the entire task engine: - -#### **Intra-Task Communication** - -- **Action A** executes and produces output -- **Action B** can reference Action A's output as a parameter -- **Action C** can reference both Action A and Action B outputs -- All within the same task execution context - -#### **Cross-Task Communication** - -- **Task 1** executes and produces output -- **Task 2** can reference Task 1's output as a parameter -- **Action** in Task 3 can reference output from Task 1 or Task 2 -- **Task 4** can reference output from any combination of actions and tasks - -#### **Mixed References** - -- Actions can reference outputs from other tasks -- Tasks can reference outputs from individual actions -- Complex workflows can be built with data flowing between any entities - -This creates a comprehensive data flow ecosystem where any entity can build upon the results of any other entity in the system. - -#### Example: Three-Action Pipeline - -```go -// Task: "file-processing" with three sequential actions -Actions: [ - readFileAction, // Action A: Reads file, produces content - transformAction, // Action B: Uses content from Action A - writeFileAction, // Action C: Uses transformed content from Action B -] - -// Execution flow: -// 1. readFileAction executes → content stored in TaskContext -// 2. transformAction executes with content from Action A → transformed content stored -// 3. writeFileAction executes with transformed content from Action B -``` - -**Yes, this system allows "task 1" containing "action a" and "action b" to utilize the result from "action a" in "action b"** - this is the core functionality the parameter passing system provides. - -## Technical Architecture - -### Core Changes - -#### 1. Action Output Interface - -- **File**: `action.go` -- **Changes**: Extend `ActionInterface` to include output methods -- **Details**: Add `GetOutput() interface{}` method to allow actions to expose their execution results -- **Integration**: Leverage existing `ResultProvider` interface for actions that produce results - -#### 2. Parameter Mapping System - -- **File**: `task.go` -- **Changes**: Add parameter mapping logic to `Task.Run()` method -- **Details**: Implement runtime parameter resolution before each action execution - -#### 3. Action Parameter Types - -- **File**: `action.go` -- **Changes**: Define parameter types and mapping interfaces -- **Details**: Create parameter structs that can reference previous action outputs - -### Implementation Details - -#### Phase 1: Data Layer Foundation - -##### 1.1 Action Output Interface - -```go -// Extend ActionInterface in action.go -type ActionInterface interface { - BeforeExecute(ctx context.Context) error - Execute(ctx context.Context) error - AfterExecute(ctx context.Context) error - GetOutput() interface{} // NEW: Return action execution results -} - -// Actions can optionally implement ResultProvider for richer result handling -type ActionWithResults interface { - ActionInterface - ResultProvider -} -``` - -##### 1.2 Parameter Types - -```go -// New types in action.go -type ActionParameter interface { - Resolve(ctx context.Context, globalContext *GlobalContext) (interface{}, error) -} - -type StaticParameter struct { - Value interface{} -} - -type ActionOutputParameter struct { - ActionID string - OutputKey string // Optional: for actions with multiple outputs -} - -// Enhanced parameter for actions implementing ResultProvider -type ActionResultParameter struct { - ActionID string - ResultKey string // Maps to ResultProvider.GetResult() or specific output field -} - -// Cross-task parameter references -type TaskOutputParameter struct { - TaskID string - OutputKey string // Optional: for tasks with multiple outputs -} - -// Mixed entity parameter references -type EntityOutputParameter struct { - EntityType string // "action" or "task" - EntityID string - OutputKey string // Optional: for entities with multiple outputs -} -``` - -##### 1.3 Global Context - -```go -// New struct in task.go (renamed from TaskContext for clarity) -type GlobalContext struct { - ActionOutputs map[string]interface{} - ActionResults map[string]ResultProvider // For actions implementing ResultProvider - TaskOutputs map[string]interface{} // For tasks that produce outputs - TaskResults map[string]ResultProvider // For tasks implementing ResultProvider - mu sync.RWMutex -} -``` - -#### Phase 2: Execution Engine - -##### 2.1 Parameter Resolution - -- **File**: `task.go` -- **Function**: `Task.resolveParameters(ctx, action, taskContext)` -- **Logic**: Resolve all parameters before action execution, replacing action references with actual values -- **ResultProvider Integration**: Check if action implements ResultProvider and store for enhanced parameter resolution - -##### 2.2 Action and Task Execution Flow - -- **File**: `task.go` -- **Function**: `Task.Run()` and `TaskManager.RunTask()` -- **Changes**: Add parameter resolution step before each action/task execution -- **Flow**: - 1. Execute action/task - 2. Store action output in global context (`GlobalContext.ActionOutputs`) - 3. Store action as ResultProvider if applicable (`GlobalContext.ActionResults`) - 4. Store task output in global context (`GlobalContext.TaskOutputs`) - 5. Store task as ResultProvider if applicable (`GlobalContext.TaskResults`) - 6. Resolve parameters for next action/task using accumulated global context - 7. Execute next action/task with resolved parameters - 8. Continue until all actions/tasks complete - -**Key Insight**: The `GlobalContext` maintains state across the entire system, allowing any entity (action or task) to access results from any other entity (action or task) that has already executed. - -#### Phase 3: Action Integration - -##### 3.1 Update Existing Actions - -- **Files**: All action files in `actions/` directory -- **Changes**: Implement `GetOutput()` method for each action -- **Optional Enhancement**: Implement `ResultProvider` interface for actions with rich result data -- **Examples**: - - `docker/docker_run_action.go`: Return container ID and status - - `file/read_file_action.go`: Return file contents - - `docker/docker_compose_ps_action.go`: Return service list - -##### 3.2 Parameter-Aware Action Constructors - -- **Files**: All action constructor functions -- **Changes**: Accept `ActionParameter` types instead of concrete values -- **Example**: - -```go -// Before -func NewRemoveFilesAction(paths []string, logger *slog.Logger) *Action[*RemoveFilesAction] - -// After -func NewRemoveFilesAction(paths ActionParameter, logger *slog.Logger) *Action[*RemoveFilesAction] -``` - -## Files to Modify - -### Core Engine Files - -1. **`action.go`** - - - Add `GetOutput()` method to `ActionInterface` - - Define parameter types and interfaces - - Update `Action[T]` struct to support parameter resolution - -2. **`task.go`** - - - Add `TaskContext` struct - - Implement parameter resolution logic - - Update `Task.Run()` method to handle parameter mapping - -3. **`interface.go`** - - Update `ActionInterface` to include output methods - - Add parameter-related interfaces - -### Action Files (All actions in `actions/` directory) - -1. **`actions/docker/*.go`** - Implement `GetOutput()` methods -2. **`actions/file/*.go`** - Implement `GetOutput()` methods -3. **`actions/system/*.go`** - Implement `GetOutput()` methods -4. **`actions/utility/*.go`** - Implement `GetOutput()` methods - -### Task Files - -1. **`tasks/*.go`** - Update task definitions to use new parameter system - -## ResultProvider Integration - -### Leveraging Existing Infrastructure - -The existing `ResultProvider` interface provides a natural extension point for the parameter passing system: - -```go -// Existing interface from interface.go -type ResultProvider interface { - GetResult() interface{} - GetError() error -} - -// Enhanced action interface that can optionally provide rich results -type ActionWithResults interface { - ActionInterface - ResultProvider -} -``` - -### Benefits of ResultProvider Integration - -1. **Established Pattern**: The interface is already well-tested and has comprehensive mocking support -2. **Error Handling**: Actions can provide both results and error information -3. **Testing Support**: Existing `ResultProviderMock` can be used for testing parameter resolution -4. **Backward Compatibility**: Actions can implement ResultProvider without breaking existing code -5. **Rich Result Data**: Actions can return complex data structures with proper error handling - -### Example Action Implementation - -```go -type DockerRunAction struct { - BaseAction - Image string - RunArgs []string - ContainerID string - ExitCode int - Error error -} - -// Implement ActionInterface -func (a *DockerRunAction) GetOutput() interface{} { - return map[string]interface{}{ - "containerID": a.ContainerID, - "exitCode": a.ExitCode, - "success": a.Error == nil, - } -} - -// Optionally implement ResultProvider for enhanced functionality -func (a *DockerRunAction) GetResult() interface{} { - return a.GetOutput() -} - -func (a *DockerRunAction) GetError() error { - return a.Error -} -``` - -## Algorithm Details - -### Parameter Resolution Algorithm - -``` -1. For each action in task: - a. Resolve all parameters using TaskContext - b. Execute action with resolved parameters - c. Store action output in TaskContext - d. Continue to next action -``` - -### Parameter Resolution Steps - -``` -1. Check if parameter is StaticParameter: - - Return Value directly -2. Check if parameter is ActionOutputParameter: - - Look up ActionID in GlobalContext.ActionOutputs - - Extract OutputKey if specified - - Return resolved value -3. Check if parameter is ActionResultParameter: - - Look up ActionID in GlobalContext.ActionResults - - Use ResultProvider.GetResult() or extract specific field - - Return resolved value -4. Check if parameter is TaskOutputParameter: - - Look up TaskID in GlobalContext.TaskOutputs - - Extract OutputKey if specified - - Return resolved value -5. Check if parameter is EntityOutputParameter: - - Determine entity type (action or task) - - Look up in appropriate GlobalContext map - - Extract OutputKey if specified - - Return resolved value -6. Handle parameter type conversion as needed -7. Return error if resolution fails -``` - -## Example Usage After Implementation - -### Intra-Task Action Communication - -The parameter passing system enables actions within the same task to share data seamlessly: - -```go -func NewFileProcessingTask(config FileProcessingConfig, logger *slog.Logger) *task_engine.Task { - return &task_engine.Task{ - ID: "file-processing", - Name: "Process and Transform Files", - Actions: []engine.ActionWrapper{ - // Action A: Read source file - file.NewReadFileAction( - config.SourcePath, - nil, // outputBuffer will be populated - logger, - ), - - // Action B: Transform content using output from Action A - file.NewReplaceLinesAction( - config.SourcePath, - map[*regexp.Regexp]string{ - regexp.MustCompile("{{content}}"): - ActionOutputParameter{ActionID: "read-file-source", OutputKey: "content"}, - }, - logger, - ), - - // Action C: Write to new location with transformed content - file.NewWriteFileAction( - config.DestinationPath, - nil, // content will come from previous action - true, // overwrite - ActionOutputParameter{ActionID: "replace-lines-action", OutputKey: "content"}, - logger, - ), - }, - Logger: logger, - } -} -``` - -### Basic Parameter Passing - -```go -func NewCleanupAgentsTask(config CleanupAgentsTaskConfig, logger *slog.Logger) *task_engine.Task { - return &task_engine.Task{ - ID: "cleanup-agents", - Name: "Cleanup Agent Files", - Actions: []engine.ActionWrapper{ - // List agent files - will produce output - agent_actions.NewListAgentFilesAction(config.AgentDirectory, logger), - - // Remove files using output from previous action - file.NewRemoveFilesAction( - ActionOutputParameter{ActionID: "list-agent-files", OutputKey: "paths"}, - logger, - ), - }, - Logger: logger, - } -} -``` - -### Enhanced ResultProvider Integration - -```go -func NewDockerDeploymentTask(config DockerDeploymentConfig, logger *slog.Logger) *task_engine.Task { - return &task_engine.Task{ - ID: "docker-deployment", - Name: "Docker Container Deployment", - Actions: []engine.ActionWrapper{ - // Pull image - returns image info and any errors - docker.NewDockerPullAction(logger, config.Images), - - // Run container using image from previous action - docker.NewDockerRunAction( - logger, - ActionResultParameter{ActionID: "docker-pull-action", ResultKey: "imageName"}, - nil, // outputBuffer - config.RunArgs..., - ), - - // Check health using container ID from run action - docker.NewCheckContainerHealthAction( - logger, - ActionResultParameter{ActionID: "docker-run-action", ResultKey: "containerID"}, - config.HealthCheckTimeout, - ), - }, - Logger: logger, - } -} -``` - -### Cross-Task Parameter Passing - -```go -// Task 1: Build and package application -func NewBuildTask(config BuildConfig, logger *slog.Logger) *task_engine.Task { - return &task_engine.Task{ - ID: "build-application", - Name: "Build and Package Application", - Actions: []engine.ActionWrapper{ - // Build actions that produce artifacts - build.NewCompileAction(config.SourceDir, logger), - build.NewPackageAction(config.OutputDir, logger), - }, - Logger: logger, - } -} - -// Task 2: Deploy using artifacts from Task 1 -func NewDeployTask(config DeployConfig, logger *slog.Logger) *task_engine.Task { - return &task_engine.Task{ - ID: "deploy-application", - Name: "Deploy Application", - Actions: []engine.ActionWrapper{ - // Deploy action using package path from build task - deploy.NewDeployAction( - TaskOutputParameter{TaskID: "build-application", OutputKey: "packagePath"}, - config.Environment, - logger, - ), - }, - Logger: logger, - } -} - -// Task 3: Monitor deployment using results from both previous tasks -func NewMonitorTask(config MonitorConfig, logger *slog.Logger) *task_engine.Task { - return &task_engine.Task{ - ID: "monitor-deployment", - Name: "Monitor Deployment Health", - Actions: []engine.ActionWrapper{ - // Monitor using deployment ID from deploy task - monitor.NewHealthCheckAction( - TaskOutputParameter{TaskID: "deploy-application", OutputKey: "deploymentID"}, - config.CheckInterval, - logger, - ), - }, - Logger: logger, - } -} -``` - -## Ergonomics and Usability Design - -### Design Principles - -The parameter passing system is designed with these ergonomic principles: - -1. **Explicit Over Implicit**: All parameter references must be explicit, no magic or hidden dependencies -2. **Consistent Syntax**: Uniform parameter syntax across all entity types -3. **Compile-Time Validation**: Catch parameter errors before runtime -4. **Clear Naming**: Parameter types have descriptive names that indicate their purpose -5. **Fail Fast**: Parameter resolution errors are caught early with helpful error messages - -### Parameter Syntax Design - -#### **Consistent Naming Convention** - -```go -// All parameter types follow the pattern: [Entity]OutputParameter -ActionOutputParameter{ActionID: "action-name", OutputKey: "key"} -TaskOutputParameter{TaskID: "task-name", OutputKey: "key"} -EntityOutputParameter{EntityType: "action", EntityID: "name", OutputKey: "key"} -``` - -#### **Required vs Optional Fields** - -```go -// ActionID/TaskID is always required -ActionOutputParameter{ActionID: "must-have"} // Valid - -// OutputKey is optional - if omitted, returns entire output -ActionOutputParameter{ActionID: "action-name"} // Returns full output -ActionOutputParameter{ActionID: "action-name", OutputKey: "specific-field"} // Returns specific field -``` - -### Error Prevention Mechanisms - -#### **1. Compile-Time Type Safety** - -```go -// Parameter types are strongly typed -type ActionOutputParameter struct { - ActionID string // Required - OutputKey string // Optional -} - -// This prevents common mistakes: -// ❌ Wrong: ActionOutputParameter{ActionID: 123} // Compile error -// ✅ Correct: ActionOutputParameter{ActionID: "action-name"} -``` - -#### **2. Validation at Construction** - -```go -// Parameter constructors validate inputs -func NewActionOutputParameter(actionID string, outputKey string) ActionOutputParameter { - if actionID == "" { - panic("ActionID cannot be empty") - } - return ActionOutputParameter{ActionID: actionID, OutputKey: outputKey} -} - -// Usage: -// ❌ Wrong: ActionOutputParameter{ActionID: ""} // Panic with clear message -// ✅ Correct: NewActionOutputParameter("action-name", "output-key") -``` - -#### **3. Runtime Validation** - -```go -// Parameter resolution validates at runtime -func (p ActionOutputParameter) Resolve(ctx context.Context, globalContext *GlobalContext) (interface{}, error) { - if p.ActionID == "" { - return nil, fmt.Errorf("ActionOutputParameter: ActionID cannot be empty") - } - - output, exists := globalContext.ActionOutputs[p.ActionID] - if !exists { - return nil, fmt.Errorf("ActionOutputParameter: action '%s' not found in context", p.ActionID) - } - - if p.OutputKey != "" { - // Validate OutputKey exists in output - if outputMap, ok := output.(map[string]interface{}); ok { - if value, exists := outputMap[p.OutputKey]; exists { - return value, nil - } - return nil, fmt.Errorf("ActionOutputParameter: output key '%s' not found in action '%s'", p.OutputKey, p.ActionID) - } - return nil, fmt.Errorf("ActionOutputParameter: action '%s' output is not a map, cannot extract key '%s'", p.ActionID, p.OutputKey) - } - - return output, nil -} -``` - -### Common Pitfalls and Prevention - -#### **1. Circular Dependencies** - -```go -// ❌ Problematic: Action A references Action B, but Action B references Action A -Actions: [ - actionA, // References actionB output - actionB, // References actionA output -] - -// ✅ Solution: Task engine detects and prevents circular references -func (t *Task) validateNoCircularDependencies() error { - // Implementation detects cycles in parameter references - return nil -} -``` - -#### **2. Missing Entity References** - -```go -// ❌ Problematic: Reference to non-existent action -ActionOutputParameter{ActionID: "non-existent-action"} - -// ✅ Solution: Clear error message at runtime -// Error: "ActionOutputParameter: action 'non-existent-action' not found in context" -``` - -#### **3. Type Mismatches** - -```go -// ❌ Problematic: Expecting string but getting int -ActionOutputParameter{ActionID: "action-name", OutputKey: "count"} -// If action outputs: {"count": 42} (int), but action expects string - -// ✅ Solution: Type validation and conversion helpers -func (p ActionOutputParameter) ResolveAsString(ctx context.Context, globalContext *GlobalContext) (string, error) { - value, err := p.Resolve(ctx, globalContext) - if err != nil { - return "", err - } - - switch v := value.(type) { - case string: - return v, nil - case int: - return strconv.Itoa(v), nil - default: - return "", fmt.Errorf("cannot convert %T to string", value) - } -} -``` - -### Helper Functions for Common Patterns - -#### **1. Type-Safe Parameter Creation** - -```go -// Helper functions for common parameter patterns -func ActionOutput(actionID string) ActionOutputParameter { - return ActionOutputParameter{ActionID: actionID} -} - -func ActionOutputField(actionID, field string) ActionOutputParameter { - return ActionOutputParameter{ActionID: actionID, OutputKey: field} -} - -func TaskOutput(taskID string) TaskOutputParameter { - return TaskOutputParameter{TaskID: taskID} -} - -func TaskOutputField(taskID, field string) TaskOutputParameter { - return TaskOutputParameter{TaskID: taskID, OutputKey: field} -} - -// Usage becomes more readable: -// Before: ActionOutputParameter{ActionID: "read-file", OutputKey: "content"} -// After: ActionOutputField("read-file", "content") -``` - -#### **2. Parameter Validation Helpers** - -```go -// Validate parameters before task execution -func (t *Task) ValidateParameters() error { - for i, action := range t.Actions { - if err := t.validateActionParameters(action, i); err != nil { - return fmt.Errorf("action %d (%s): %w", i, action.GetName(), err) - } - } - return nil -} - -func (t *Task) validateActionParameters(action ActionWrapper, index int) error { - // Implementation validates all parameters can be resolved - return nil -} -``` - -### Best Practices for Downstream Clients - -#### **1. Naming Conventions** - -```go -// ✅ Good: Use descriptive, consistent names -Actions: [ - file.NewReadFileAction("config.yaml", nil, logger), // ID: "read-config" - yaml.NewParseYamlAction(nil, logger), // ID: "parse-yaml" - config.NewValidateConfigAction(nil, logger), // ID: "validate-config" -] - -// Reference with clear names -ActionOutputParameter{ActionID: "read-config", OutputKey: "content"} -ActionOutputParameter{ActionID: "parse-yaml", OutputKey: "parsed"} -ActionOutputParameter{ActionID: "validate-config", OutputKey: "isValid"} -``` - -#### **2. Output Structure Design** - -```go -// ✅ Good: Consistent output structure across actions -func (a *ReadFileAction) GetOutput() interface{} { - return map[string]interface{}{ - "content": a.Content, - "fileSize": a.FileSize, - "readTime": a.ReadTime, - "success": a.Error == nil, - } -} - -// ❌ Avoid: Inconsistent or unclear output structures -func (a *ReadFileAction) GetOutput() interface{} { - return a.Content // Just returns raw content, no metadata -} -``` - -#### **3. Parameter Validation** - -```go -// ✅ Good: Validate parameters early in task creation -func NewFileProcessingTask(config FileConfig, logger *slog.Logger) *task_engine.Task { - task := &task_engine.Task{ - // ... task configuration - } - - // Validate parameters before returning - if err := task.ValidateParameters(); err != nil { - logger.Error("Invalid task parameters", "error", err) - panic(fmt.Sprintf("Task validation failed: %v", err)) - } - - return task -} -``` - -#### **4. Error Handling in Parameter Resolution** - -```go -// ✅ Good: Handle parameter resolution errors gracefully -func (a *ProcessFileAction) Execute(ctx context.Context) error { - content, err := a.ContentParam.Resolve(ctx, a.globalContext) - if err != nil { - return fmt.Errorf("failed to resolve content parameter: %w", err) - } - - // Type assertion with error handling - contentStr, ok := content.(string) - if !ok { - return fmt.Errorf("content parameter is not a string, got %T", content) - } - - // Process content... - return nil -} -``` - -#### **5. Documentation and Examples** - -```go -// ✅ Good: Document expected parameter types and outputs -type ReadFileAction struct { - BaseAction - FilePath string - Content string - Error error -} - -// GetOutput returns a map with the following structure: -// { -// "content": string, // File contents -// "fileSize": int64, // File size in bytes -// "readTime": time.Time, // When file was read -// "success": bool // Whether read was successful -// } -func (a *ReadFileAction) GetOutput() interface{} { - return map[string]interface{}{ - "content": a.Content, - "fileSize": a.FileSize, - "readTime": a.ReadTime, - "success": a.Error == nil, - } -} -``` - -### Common Usage Patterns - -#### **1. Simple Data Flow** - -```go -// Read file → Process content → Write result -Actions: [ - file.NewReadFileAction("input.txt", nil, logger), - process.NewTransformAction( - ActionOutputParameter{ActionID: "read-file", OutputKey: "content"}, - "uppercase", - logger, - ), - file.NewWriteFileAction( - "output.txt", - ActionOutputParameter{ActionID: "transform", OutputKey: "result"}, - true, - logger, - ), -] -``` - -#### **2. Conditional Processing** - -```go -// Read file → Check if valid → Process if valid -Actions: [ - file.NewReadFileAction("data.json", nil, logger), - json.NewValidateAction( - ActionOutputParameter{ActionID: "read-file", OutputKey: "content"}, - logger, - ), - process.NewConditionalAction( - ActionOutputParameter{ActionID: "validate", OutputKey: "isValid"}, - ActionOutputParameter{ActionID: "read-file", OutputKey: "content"}, - logger, - ), -] -``` - -#### **3. Cross-Task Workflows** - -```go -// Build → Deploy → Monitor -// Task 1: Build -func NewBuildTask(config BuildConfig, logger *slog.Logger) *task_engine.Task { - return &task_engine.Task{ - ID: "build-app", - Actions: [/* build actions */], - Logger: logger, - } -} - -// Task 2: Deploy (uses build output) -func NewDeployTask(config DeployConfig, logger *slog.Logger) *task_engine.Task { - return &task_engine.Task{ - ID: "deploy-app", - Actions: []engine.ActionWrapper{ - deploy.NewDeployAction( - TaskOutputParameter{TaskID: "build-app", OutputKey: "packagePath"}, - config.Environment, - logger, - ), - }, - Logger: logger, - } -} -``` - -## Benefits - -1. **Decoupled Actions**: Actions no longer need to know about each other -2. **Runtime Data Flow**: Parameter resolution happens at execution time -3. **Reusable Actions**: Actions can be used in different task contexts -4. **Type Safety**: Parameter types are defined and validated -5. **Cleaner Task Definitions**: Tasks become more declarative and easier to understand -6. **Leverages Existing Infrastructure**: Integrates with established `ResultProvider` interface -7. **Enhanced Result Handling**: Actions can provide rich result data with error information -8. **Intra-Task Data Flow**: Actions within the same task can build upon each other's results -9. **Cross-Task Data Flow**: Tasks can reference outputs from other tasks -10. **Mixed Entity References**: Actions can reference task outputs, tasks can reference action outputs -11. **Comprehensive Workflows**: Build complex multi-task pipelines with data flowing between any entities -12. **Pipeline Execution**: Create sophisticated workflows where each step feeds into the next across the entire system -13. **Intuitive Ergonomics**: Clear, consistent parameter syntax that's hard to misuse -14. **Compile-Time Safety**: Parameter validation and type checking to catch errors early - -## Migration Strategy - -1. **Phase 1**: Implement core parameter system without breaking existing code -2. **Phase 2**: Update action constructors to accept both old and new parameter types -3. **Phase 3**: Gradually migrate existing tasks to use new parameter system -4. **Phase 4**: Remove deprecated parameter passing methods -5. **Phase 5**: Implement ergonomic improvements for ID and output key management - -### Phase 5: Ergonomic Improvements - -#### 5.1 Instance-Based ID Fetching - -**Problem**: Currently, users must manually specify string IDs for actions and tasks, which is error-prone and creates maintenance overhead. - -**Solution**: Auto-generated IDs with optional override capability. - -```go -// Enhanced ActionWrapper interface -type ActionWrapper interface { - ActionInterface - GetID() string - SetID(string) // Optional: allow manual override - GetName() string -} - -// Enhanced Action struct -type Action[T ActionInterface] struct { - Wrapped T - ID string - Name string - // ... existing fields -} - -// Auto-generate ID if not provided -func NewAction[T ActionInterface](wrapped T, name string, id ...string) *Action[T] { - actionID := name - if len(id) > 0 && id[0] != "" { - actionID = id[0] - } - - return &Action[T]{ - Wrapped: wrapped, - ID: actionID, - Name: name, - } -} - -// Usage becomes much simpler: -// Before: file.NewReadFileAction("config.yaml", nil, logger) // Must manually track ID -// After: file.NewReadFileAction("config.yaml", nil, logger) // ID auto-generated as "read-file" -``` - -**Benefits**: - -- No more manual string management for IDs -- Automatic ID generation based on action names -- Optional manual override when needed -- Reduced chance of ID conflicts or typos - -#### 5.2 Type-Safe Output Key Management - -**Problem**: Output keys are currently string-based, making them error-prone and not type-safe. - -**Solution**: Generic approach with type constraints and compile-time validation. - -```go -// Generic constraint for output types -type OutputType interface { - // Marker interface - any struct can implement this - // We'll use reflection or code generation to validate the structure -} - -// Constraint for actions that produce specific output types -type ActionWithOutput[T OutputType] interface { - ActionInterface - GetOutput() T -} - -// Enhanced Action struct with generic output type -type Action[T ActionInterface, O OutputType] struct { - Wrapped T - ID string - Name string - Output O - // ... existing fields -} - -// Type-safe output key references -type TypedOutputKey[T OutputType] struct { - ActionID string - Key string -} - -// Compile-time validation of output keys -func (k TypedOutputKey[T]) Validate() error { - // Use reflection to validate that Key exists in T - t := reflect.TypeOf((*T)(nil)).Elem() - if t.Kind() == reflect.Struct { - _, exists := t.FieldByName(k.Key) - if !exists { - return fmt.Errorf("field '%s' does not exist in output type %T", k.Key, (*T)(nil)) - } - } - return nil -} - -// Usage with type safety: -type FileReadOutput struct { - Content string - FileSize int64 - ReadTime time.Time - Success bool -} - -// Action with typed output -readAction := file.NewReadFileAction[FileReadOutput]("config.yaml", nil, logger) - -// Type-safe output key reference -contentKey := TypedOutputKey[FileReadOutput]{ - ActionID: readAction.GetID(), - Key: "Content", // Compile-time validation that this field exists -} - -// Parameter using typed key -param := ActionOutputParameter{ - ActionID: readAction.GetID(), - OutputKey: "Content", // Type-safe reference -} -``` - -**Alternative Approach**: Code generation for compile-time validation. - -```go -//go:generate go run github.com/vektra/mockery/v2 --name ActionInterface --output ./mocks - -// Generated code ensures output keys are valid at compile time -type GeneratedOutputKeys struct { - ReadFileAction struct { - Content string - FileSize int64 - ReadTime time.Time - Success bool - } - DockerRunAction struct { - ContainerID string - ExitCode int - Success bool - } -} - -// Usage with generated keys -contentKey := ActionOutputParameter{ - ActionID: readAction.GetID(), - OutputKey: GeneratedOutputKeys.ReadFileAction.Content, // Compile-time safe -} -``` - -**Benefits**: - -- Compile-time validation of output keys -- Type safety prevents runtime errors -- Better IDE support with autocomplete -- Refactoring safety (renaming fields updates all references) - -#### 5.3 Implementation Strategy for Phase 5 - -**Step 1**: Extend Action Interface - -```go -// Update action.go -type ActionInterface interface { - BeforeExecute(ctx context.Context) error - Execute(ctx context.Context) error - AfterExecute(ctx context.Context) error - GetOutput() interface{} - GetID() string // NEW - SetID(string) // NEW - GetName() string // NEW -} -``` - -**Step 2**: Update Action Constructor Pattern - -```go -// New pattern for all action constructors -func NewReadFileAction[T OutputType](filePath string, outputBuffer T, logger *slog.Logger, id ...string) *Action[*ReadFileAction, T] { - actionID := "read-file" - if len(id) > 0 && id[0] != "" { - actionID = id[0] - } - - return &Action[*ReadFileAction, T]{ - Wrapped: &ReadFileAction{ - FilePath: filePath, - Output: outputBuffer, - Logger: logger, - }, - ID: actionID, - Name: "Read File", - } -} -``` - -**Step 3**: Update Task Definition Pattern - -```go -// New task definition pattern -func NewFileProcessingTask(config FileConfig, logger *slog.Logger) *task_engine.Task { - readAction := file.NewReadFileAction[FileReadOutput]("input.txt", nil, logger) - processAction := process.NewTransformAction[ProcessedOutput]( - ActionOutputParameter{ - ActionID: readAction.GetID(), // Auto-generated ID - OutputKey: "Content", // Type-safe key - }, - "uppercase", - logger, - ) - - return &task_engine.Task{ - ID: "file-processing", - Name: "Process and Transform Files", - Actions: []engine.ActionWrapper{readAction, processAction}, - Logger: logger, - } -} -``` - -**Step 4**: Backward Compatibility - -```go -// Maintain backward compatibility during transition -func NewReadFileAction(filePath string, outputBuffer interface{}, logger *slog.Logger) *Action[*ReadFileAction] { - // Legacy constructor - delegates to new generic constructor - return NewReadFileAction[interface{}](filePath, outputBuffer, logger) -} -``` - -#### 5.4 Migration Benefits - -**For Developers**: - -- No more manual ID string management -- Compile-time validation of output keys -- Better IDE support and autocomplete -- Reduced chance of runtime errors - -**For Maintainers**: - -- Easier refactoring (renaming fields updates all references) -- Better code organization and type safety -- Reduced debugging time from ID/key mismatches - -**For Users**: - -- More intuitive API -- Better error messages at compile time -- Consistent naming conventions - -## Testing Strategy - -1. **Unit Tests**: Test parameter resolution logic in isolation -2. **Integration Tests**: Test parameter passing between actions in real tasks -3. **Backward Compatibility Tests**: Ensure existing tasks continue to work -4. **Performance Tests**: Measure impact of parameter resolution on task execution time diff --git a/docs/features/0002_REVIEW.md b/docs/features/0002_REVIEW.md deleted file mode 100644 index 1dd580c..0000000 --- a/docs/features/0002_REVIEW.md +++ /dev/null @@ -1,289 +0,0 @@ -# Feature 0002: Action Parameter Passing - Code Review - -## Executive Summary - -The Action Parameter Passing feature has been **successfully implemented** with comprehensive coverage of all planned functionality. The implementation follows the architectural design outlined in the plan and includes extensive testing. All core components are working correctly with proper error handling and validation. - -## Implementation Status Summary - -| Component | Status | Implementation Quality | Test Coverage | -| ---------------------- | ----------- | ---------------------- | ------------- | -| **Core Engine** | ✅ Complete | Excellent | Comprehensive | -| **Parameter Types** | ✅ Complete | Excellent | Comprehensive | -| **Global Context** | ✅ Complete | Excellent | Comprehensive | -| **Action Integration** | ✅ Complete | Excellent | Comprehensive | -| **Task Execution** | ✅ Complete | Excellent | Comprehensive | -| **Docker Actions** | ✅ Complete | Excellent | Comprehensive | -| **File Actions** | ✅ Complete | Excellent | Comprehensive | -| **System Actions** | ✅ Complete | Excellent | Comprehensive | -| **Utility Actions** | ✅ Complete | Excellent | Comprehensive | -| **Testing** | ✅ Complete | Excellent | Comprehensive | - -## Detailed Implementation Analysis - -### ✅ **Core Engine Files - COMPLETE** - -#### 1. `action.go` - **FULLY IMPLEMENTED** - -- **ActionInterface**: Extended with `GetOutput() interface{}` method ✅ -- **ActionParameter Interface**: Complete with all planned parameter types ✅ -- **Parameter Types**: All 5 parameter types implemented with full validation ✅ -- **GlobalContext**: Complete implementation with thread-safe operations ✅ -- **Helper Functions**: All planned helper functions implemented ✅ -- **Phase 5 Ergonomics**: TypedOutputKey with runtime validation implemented ✅ - -**Implementation Quality**: Excellent - -- Proper error handling and validation -- Thread-safe operations with mutex protection -- Comprehensive parameter resolution logic -- Clean, consistent API design - -#### 2. `task.go` - **FULLY IMPLEMENTED** - -- **Parameter Resolution**: Complete integration with GlobalContext ✅ -- **Action Output Storage**: Automatic storage of action outputs ✅ -- **Task Output Storage**: Automatic storage of task outputs ✅ -- **Parameter Validation**: Pre-execution validation implemented ✅ -- **Context Management**: Proper GlobalContext integration ✅ - -**Implementation Quality**: Excellent - -- Clean separation of concerns -- Proper error handling and logging -- Efficient parameter resolution -- Thread-safe operations - -#### 3. `task_manager.go` - **FULLY IMPLEMENTED** - -- **Global Context Management**: Complete integration ✅ -- **Cross-Task Parameter Passing**: Full support implemented ✅ -- **Context Persistence**: Maintains context across task executions ✅ - -**Implementation Quality**: Excellent - -- Proper context management -- Clean API design -- Efficient resource handling - -#### 4. `interface.go` - **FULLY IMPLEMENTED** - -- **ResultProvider Interface**: Leveraged for enhanced functionality ✅ -- **Task Manager Interface**: Extended with context management ✅ - -**Implementation Quality**: Excellent - -- Clean interface design -- Proper abstraction layers - -### ✅ **Action Files - COMPLETE** - -#### Docker Actions - **FULLY IMPLEMENTED** - -All Docker actions have been updated with: - -- **GetOutput() Methods**: Comprehensive output structures ✅ -- **Parameter Support**: Full ActionParameter integration ✅ -- **Parameter-Aware Constructors**: Both legacy and new constructors ✅ -- **Runtime Parameter Resolution**: Proper GlobalContext integration ✅ - -**Actions Updated**: - -- `docker_compose_ls_action.go` ✅ -- `docker_compose_ps_action.go` ✅ -- `docker_compose_exec_action.go` ✅ -- `docker_compose_up_action.go` ✅ -- `docker_compose_down_action.go` ✅ -- `docker_run_action.go` ✅ -- `docker_pull_action.go` ✅ -- `docker_generic_action.go` ✅ -- `docker_ps_action.go` ✅ -- `docker_image_list_action.go` ✅ -- `docker_image_rm_action.go` ✅ -- `docker_load_action.go` ✅ -- `check_container_health_action.go` ✅ -- `docker_status_action.go` ✅ - -#### File Actions - **FULLY IMPLEMENTED** - -All file actions have been updated with: - -- **GetOutput() Methods**: Comprehensive output structures ✅ -- **Parameter Support**: Full ActionParameter integration ✅ -- **Parameter-Aware Constructors**: Both legacy and new constructors ✅ - -**Actions Updated**: - -- `read_file_action.go` ✅ -- `write_file_action.go` ✅ -- `copy_file_action.go` ✅ -- `move_file_action.go` ✅ -- `delete_path_action.go` ✅ -- `create_directories_action.go` ✅ -- `create_symlink_action.go` ✅ -- `replace_lines_action.go` ✅ - -#### System Actions - **FULLY IMPLEMENTED** - -All system actions have been updated with: - -- **GetOutput() Methods**: Comprehensive output structures ✅ -- **Parameter Support**: Full ActionParameter integration ✅ - -**Actions Updated**: - -- `service_status_action.go` ✅ -- `shutdown_action.go` ✅ - -#### Utility Actions - **FULLY IMPLEMENTED** - -All utility actions have been updated with: - -- **GetOutput() Methods**: Comprehensive output structures ✅ -- **Parameter Support**: Full ActionParameter integration ✅ - -**Actions Updated**: - -- `fetch_interfaces_action.go` ✅ -- `read_mac_action.go` ✅ -- `prerequisite_check_action.go` ✅ -- `wait_action.go` ✅ - -### ✅ **Testing - COMPLETE** - -#### Test Coverage - **COMPREHENSIVE** - -- **Unit Tests**: All parameter types and resolution logic ✅ -- **Integration Tests**: Full parameter passing workflows ✅ -- **Action Tests**: All actions have comprehensive test coverage ✅ -- **Error Handling Tests**: All error scenarios covered ✅ -- **Parameter Resolution Tests**: All parameter types tested ✅ - -#### Test Quality - **EXCELLENT** - -- **Mock Integration**: Proper use of mocks for testing ✅ -- **Parameter Validation**: All parameter scenarios tested ✅ -- **Error Scenarios**: Comprehensive error handling tests ✅ -- **Cross-Entity References**: Full testing of parameter references ✅ - -## Architecture Compliance - -### ✅ **Plan Requirements - FULLY MET** - -#### Phase 1: Data Layer Foundation ✅ - -- **Action Output Interface**: Complete implementation ✅ -- **Parameter Types**: All 5 types implemented ✅ -- **Global Context**: Full implementation with thread safety ✅ - -#### Phase 2: Execution Engine ✅ - -- **Parameter Resolution**: Complete integration ✅ -- **Action Execution**: Full parameter resolution ✅ -- **Output Storage**: Automatic storage in GlobalContext ✅ - -#### Phase 3: Action Integration ✅ - -- **All Actions Updated**: 100% coverage ✅ -- **GetOutput() Methods**: All actions implement ✅ -- **Parameter Support**: Full integration ✅ - -#### Phase 4: Migration Strategy ✅ - -- **Backward Compatibility**: Maintained throughout ✅ -- **Legacy Constructors**: All preserved ✅ -- **New Constructors**: All implemented ✅ - -#### Phase 5: Ergonomics ✅ - -- **Helper Functions**: All implemented ✅ -- **Type Safety**: Runtime validation implemented ✅ -- **Consistent API**: Clean, intuitive design ✅ - -## Code Quality Assessment - -### ✅ **Strengths** - -1. **Architectural Excellence** - - - Clean separation of concerns - - Proper abstraction layers - - Consistent design patterns - -2. **Implementation Quality** - - - Comprehensive error handling - - Proper validation at all levels - - Thread-safe operations - -3. **Testing Coverage** - - - 100% test coverage for core functionality - - Comprehensive parameter testing - - Full integration test coverage - -4. **Backward Compatibility** - - - All existing functionality preserved - - Clean migration path - - No breaking changes - -5. **Performance Considerations** - - Efficient parameter resolution - - Minimal overhead - - Proper resource management - -### ✅ **No Critical Issues Found** - -- **No Bugs**: All functionality working correctly -- **No Data Alignment Issues**: Consistent data structures throughout -- **No Over-Engineering**: Clean, focused implementation -- **No Style Inconsistencies**: Consistent with codebase patterns - -## Minor Observations - -### 🔍 **Style Consistency** - -- All actions follow consistent patterns -- Parameter resolution follows established conventions -- Error handling is uniform across all implementations - -### 🔍 **Documentation** - -- Code is self-documenting -- Clear method names and structure -- Consistent parameter naming conventions - -## Recommendations - -### ✅ **No Changes Required** - -The implementation is production-ready and meets all requirements. - -### 💡 **Future Enhancements** (Optional) - -1. **Performance Monitoring**: Add metrics for parameter resolution performance -2. **Caching**: Consider caching for frequently resolved parameters -3. **Validation**: Add compile-time validation for parameter types (code generation) - -## Conclusion - -**Feature 0002: Action Parameter Passing has been successfully implemented with excellent quality and comprehensive coverage.** - -### **Implementation Status: ✅ COMPLETE** - -- All planned functionality implemented -- Comprehensive testing coverage -- Production-ready quality -- Full backward compatibility -- Clean, maintainable code - -### **Quality Rating: ⭐⭐⭐⭐⭐ (5/5)** - -- **Architecture**: Excellent -- **Implementation**: Excellent -- **Testing**: Excellent -- **Documentation**: Excellent -- **Maintainability**: Excellent - -The feature is ready for production use and provides a solid foundation for future enhancements. All tests pass, the code is clean and well-structured, and the implementation follows best practices throughout. diff --git a/docs/features/parameter_passing.md b/docs/features/parameter_passing.md deleted file mode 100644 index 5f6b87b..0000000 --- a/docs/features/parameter_passing.md +++ /dev/null @@ -1,119 +0,0 @@ -# Action Parameter Passing - -Actions can reference outputs from previous actions and tasks using declarative parameters. - -## Quick Examples - -### Action-to-Action Parameter Passing - -```go -func NewFileProcessingTask(logger *slog.Logger) *task_engine.Task { - return &task_engine.Task{ - ID: "file-processing", - Actions: []task_engine.ActionWrapper{ - file.NewReadFileAction("input.txt", nil, logger), - file.NewReplaceLinesAction( - "input.txt", - map[*regexp.Regexp]string{ - regexp.MustCompile("{{content}}"): - task_engine.ActionOutput("read-file", "content"), - }, - logger, - ), - }, - Logger: logger, - } -} -``` - -### Cross-Task Parameter Passing - -```go -// Build task -buildTask := &task_engine.Task{ - ID: "build-app", - Actions: []task_engine.ActionWrapper{ - docker.NewDockerBuildAction("Dockerfile", ".", logger), - }, - Logger: logger, -} - -// Deploy task using build output -deployTask := &task_engine.Task{ - ID: "deploy-app", - Actions: []task_engine.ActionWrapper{ - docker.NewDockerRunAction( - task_engine.TaskOutput("build-app", "imageID"), - []string{"-p", "8080:8080"}, - logger, - ), - }, - Logger: logger, -} - -// Execute with shared context -manager := task_engine.NewTaskManager(logger) -globalCtx := task_engine.NewGlobalContext() - -buildID := manager.AddTask(buildTask) -deployID := manager.AddTask(deployTask) - -manager.RunTaskWithContext(ctx, buildID, globalCtx) -manager.RunTaskWithContext(ctx, deployID, globalCtx) -``` - -## Parameter Types - -| Type | Purpose | Example | -| ----------------------- | ---------------- | --------------------------------------------------------------------------------------- | -| `StaticParameter` | Fixed values | `StaticParameter{Value: "/tmp/file"}` | -| `ActionOutputParameter` | Action outputs | `ActionOutputParameter{ActionID: "read-file", OutputKey: "content"}` | -| `TaskOutputParameter` | Task outputs | `TaskOutputParameter{TaskID: "build", OutputKey: "packagePath"}` | -| `EntityOutputParameter` | Mixed references | `EntityOutputParameter{EntityType: "action", EntityID: "process", OutputKey: "result"}` | - -## Helper Functions - -```go -// Action output references -task_engine.ActionOutput("action-id") -task_engine.ActionOutputField("action-id", "field-name") - -// Task output references -task_engine.TaskOutput("task-id") -task_engine.TaskOutputField("task-id", "field-name") -``` - -## Implementation - -### Add GetOutput() to Actions - -```go -func (a *MyAction) GetOutput() interface{} { - return map[string]interface{}{ - "result": a.Result, - "success": a.Error == nil, - } -} -``` - -### Use Parameters in Actions - -```go -func (a *MyAction) Execute(ctx context.Context) error { - value, err := a.Param.Resolve(ctx, a.globalContext) - if err != nil { - return fmt.Errorf("parameter resolution failed: %w", err) - } - - strValue, ok := value.(string) - if !ok { - return fmt.Errorf("expected string, got %T", value) - } - - return a.process(strValue) -} -``` - -## Examples - -See [examples/parameter_passing_examples.md](examples/parameter_passing_examples.md) for comprehensive examples. diff --git a/testing/README.md b/testing/README.md index 50e738a..4f8d0f0 100644 --- a/testing/README.md +++ b/testing/README.md @@ -1,135 +1,58 @@ # Testing Utilities -This directory contains comprehensive testing utilities and tools for the task-engine library. +Testing utilities for the task-engine library. -## Contents +## Quick Start -### Performance Testing Framework - -The `performance_testing.go` file provides a comprehensive performance testing and benchmarking framework for the task-engine library. - -#### Features - -- **Performance Benchmarking**: Measure execution time and throughput for tasks -- **Load Testing**: Simulate high-load scenarios with controlled concurrency -- **Stress Testing**: Push the system to its limits to find breaking points -- **Comprehensive Metrics**: Track execution times, throughput, error rates, and more - -#### Usage +### Mocks ```go -import "github.com/ndizazzo/task-engine/testing" - -// Create a performance tester -tester := testing.NewPerformanceTester(taskManager, logger) - -// Run benchmarks -metrics := tester.BenchmarkTaskExecution(ctx, task, iterations, concurrent) - -// Run load tests -loadMetrics := tester.LoadTest(ctx, task, iterations, concurrency, duration) +import "github.com/ndizazzo/task-engine/testing/mocks" -// Run stress tests -stressMetrics := tester.StressTest(ctx, task, rounds, iterations, duration) +mockManager := mocks.NewEnhancedTaskManagerMock() +mockRunner := &mocks.MockCommandRunner{} ``` -### Testable Task Manager - -The `testable_manager.go` file provides an enhanced version of the TaskManager specifically designed for testing scenarios. - -#### Features - -- **Testing Hooks**: Set callbacks for task lifecycle events (added, started, completed, stopped) -- **Result Override**: Override expected results, errors, and timing for testing -- **Call Tracking**: Track all method calls for verification -- **State Management**: Enhanced state management and cleanup for tests -- **Integration Testing**: Seamless integration with the main TaskManager - -#### Usage +### Testable Manager ```go import "github.com/ndizazzo/task-engine/testing" -// Create a testable task manager tm := testing.NewTestableTaskManager(logger) -// Set up testing hooks +// Set hooks tm.SetTaskAddedHook(func(task *task_engine.Task) { - // Custom logic when tasks are added + // Custom logic }) -tm.SetTaskCompletedHook(func(taskID string, err error) { - // Custom logic when tasks complete -}) - -// Override expected results for testing +// Override results tm.OverrideTaskResult("task1", "expected result") -tm.OverrideTaskError("task2", errors.New("expected error")) - -// Track method calls -addedCalls := tm.GetTaskAddedCalls() -startedCalls := tm.GetTaskStartedCalls() - -// Clear test data between tests -tm.ClearTestData() -tm.ResetToCleanState() ``` -### Mock Implementations - -The `mocks/` directory contains comprehensive mock implementations for testing: - -- **TaskManagerMock**: Mock implementation of TaskManagerInterface -- **CommandMock**: Mock implementation of CommandInterface -- **Enhanced Mock Tests**: Advanced mocking patterns and examples - -#### Usage +### Performance Testing ```go -import "github.com/ndizazzo/task-engine/testing/mocks" +tester := testing.NewPerformanceTester(taskManager, logger) -// Create mock implementations -mockTaskManager := &mocks.TaskManagerMock{} -mockCommand := &mocks.MockCommandRunner{} +// Run benchmarks +metrics := tester.BenchmarkTaskExecution(ctx, task, 100, 5) -// Set up expectations -mockTaskManager.On("AddTask", mock.AnythingOfType("*task_engine.Task")).Return(nil) -mockCommand.On("RunCommand", "echo", "hello").Return("hello", nil) +// Load test +loadMetrics := tester.LoadTest(ctx, task, 1000, 10, time.Minute) ``` -### Test Data - -The `testdata/` directory contains test fixtures and sample data: +## Components -- **Compressed Files**: Sample compressed archives for testing extraction -- **Text Files**: Sample text files for testing file operations -- **Other Fixtures**: Various test data files for different test scenarios - -## Directory Structure - -``` -testing/ -├── README.md # This documentation -├── performance_testing.go # Performance testing framework -├── testable_manager.go # Enhanced testable task manager -├── testable_manager_test.go # Tests for testable manager -├── mocks/ # Mock implementations -│ ├── task_manager_mock.go # TaskManager mock -│ ├── command_mock.go # Command mock -│ └── enhanced_mock_test.go # Advanced mocking examples -└── testdata/ # Test fixtures and data - ├── compressed.tar.gz # Sample compressed file - └── test.txt # Sample text file -``` +- **Mocks**: Complete mock implementations for all interfaces +- **Testable Manager**: Enhanced TaskManager with testing hooks +- **Performance Testing**: Built-in benchmarking and load testing +- **Test Data**: Sample files and fixtures ## Best Practices -1. **Use TestableTaskManager** for integration tests that need real TaskManager behavior -2. **Use Mocks** for unit tests that need to isolate specific components -3. **Use Performance Testing** for benchmarking and load testing scenarios -4. **Clean Up** test data between tests using `ClearTestData()` or `ResetToCleanState()` -5. **Set Hooks** early in test setup to capture all relevant events - -## Examples +1. Use `TestableTaskManager` for integration tests +2. Use mocks for unit tests +3. Clean up test data between tests +4. Set hooks early in test setup -See the individual test files for comprehensive examples of how to use each testing utility effectively. +See individual test files for complete examples.