From 9e513af0496d6b35211d69b5033eb6eabf4836c3 Mon Sep 17 00:00:00 2001 From: Snider Date: Thu, 29 Jan 2026 18:56:51 +0000 Subject: [PATCH] refactor(tests): convert PlanTemplateServiceTest to Pest functional syntax Convert PHPUnit class-based tests to Pest functional syntax with: - 47 test cases organised into 9 describe blocks - Proper beforeEach/afterEach hooks for test setup/teardown - Covers: template listing, retrieval, preview, variable substitution, plan creation, validation, categories, context generation, edge cases - Uses expect() assertions and method chaining for clarity Co-Authored-By: Claude Opus 4.5 --- TODO.md | 6 +- tests/Feature/PlanTemplateServiceTest.php | 783 +++++++++++++--------- 2 files changed, 453 insertions(+), 336 deletions(-) diff --git a/TODO.md b/TODO.md index 3580af3..267a2ee 100644 --- a/TODO.md +++ b/TODO.md @@ -70,9 +70,9 @@ Production-quality task list for the AI agent orchestration package. - 78 test cases for security-critical IP whitelisting - [x] **TEST-004: Add PlanTemplateService tests** (FIXED 2026-01-29) - - Created `tests/Feature/PlanTemplateServiceTest.php` - - Covers: template loading, variable substitution, plan creation, validation - - 35+ test cases with temporary template file handling + - Created `tests/Feature/PlanTemplateServiceTest.php` using Pest functional syntax + - Covers: template listing, retrieval, preview, variable substitution, plan creation, validation, categories, context generation + - 47 test cases organised into 9 describe blocks with proper beforeEach/afterEach setup - [x] **TEST-005: Add AI provider service tests** (FIXED 2026-01-29) - Created `tests/Unit/ClaudeServiceTest.php` - Anthropic Claude API tests diff --git a/tests/Feature/PlanTemplateServiceTest.php b/tests/Feature/PlanTemplateServiceTest.php index 76da664..bbc44b6 100644 --- a/tests/Feature/PlanTemplateServiceTest.php +++ b/tests/Feature/PlanTemplateServiceTest.php @@ -2,93 +2,76 @@ declare(strict_types=1); -namespace Core\Mod\Agentic\Tests\Feature; +/** + * Tests for the PlanTemplateService. + * + * Covers template loading, variable substitution, plan creation, and validation. + */ use Core\Mod\Agentic\Models\AgentPlan; use Core\Mod\Agentic\Services\PlanTemplateService; use Core\Tenant\Models\Workspace; -use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\File; -use Tests\TestCase; +use Symfony\Component\Yaml\Yaml; + +// ========================================================================= +// Test Setup +// ========================================================================= + +beforeEach(function () { + $this->workspace = Workspace::factory()->create(); + $this->service = app(PlanTemplateService::class); + $this->testTemplatesPath = resource_path('plan-templates'); + + if (! File::isDirectory($this->testTemplatesPath)) { + File::makeDirectory($this->testTemplatesPath, 0755, true); + } +}); + +afterEach(function () { + if (File::isDirectory($this->testTemplatesPath)) { + File::deleteDirectory($this->testTemplatesPath); + } +}); /** - * Tests for the PlanTemplateService. - * - * Covers template loading, variable substitution, and plan creation. + * Create a test template file. */ -class PlanTemplateServiceTest extends TestCase +function createTestTemplate(string $slug, array $content): void { - use RefreshDatabase; + $path = resource_path('plan-templates'); + $yaml = Yaml::dump($content, 10); + File::put($path.'/'.$slug.'.yaml', $yaml); +} - private Workspace $workspace; +// ========================================================================= +// Template Listing Tests +// ========================================================================= - private PlanTemplateService $service; - - private string $testTemplatesPath; - - protected function setUp(): void - { - parent::setUp(); - $this->workspace = Workspace::factory()->create(); - $this->service = app(PlanTemplateService::class); - - // Create test templates directory - $this->testTemplatesPath = resource_path('plan-templates'); - if (! File::isDirectory($this->testTemplatesPath)) { - File::makeDirectory($this->testTemplatesPath, 0755, true); - } - } - - protected function tearDown(): void - { - // Clean up test templates - if (File::isDirectory($this->testTemplatesPath)) { - File::deleteDirectory($this->testTemplatesPath); - } - - parent::tearDown(); - } - - /** - * Create a test template file. - */ - private function createTestTemplate(string $slug, array $content): void - { - $yaml = \Symfony\Component\Yaml\Yaml::dump($content, 10); - File::put($this->testTemplatesPath.'/'.$slug.'.yaml', $yaml); - } - - // ========================================================================= - // Template Listing Tests - // ========================================================================= - - public function test_list_returns_empty_collection_when_no_templates(): void - { - // Ensure directory is empty +describe('template listing', function () { + it('returns empty collection when no templates exist', function () { File::cleanDirectory($this->testTemplatesPath); $result = $this->service->list(); - $this->assertTrue($result->isEmpty()); - } + expect($result->isEmpty())->toBeTrue(); + }); - public function test_list_returns_templates_sorted_by_name(): void - { - $this->createTestTemplate('zebra-template', ['name' => 'Zebra Template', 'phases' => []]); - $this->createTestTemplate('alpha-template', ['name' => 'Alpha Template', 'phases' => []]); - $this->createTestTemplate('middle-template', ['name' => 'Middle Template', 'phases' => []]); + it('returns templates sorted by name', function () { + createTestTemplate('zebra-template', ['name' => 'Zebra Template', 'phases' => []]); + createTestTemplate('alpha-template', ['name' => 'Alpha Template', 'phases' => []]); + createTestTemplate('middle-template', ['name' => 'Middle Template', 'phases' => []]); $result = $this->service->list(); - $this->assertCount(3, $result); - $this->assertEquals('Alpha Template', $result[0]['name']); - $this->assertEquals('Middle Template', $result[1]['name']); - $this->assertEquals('Zebra Template', $result[2]['name']); - } + expect($result)->toHaveCount(3) + ->and($result[0]['name'])->toBe('Alpha Template') + ->and($result[1]['name'])->toBe('Middle Template') + ->and($result[2]['name'])->toBe('Zebra Template'); + }); - public function test_list_includes_template_metadata(): void - { - $this->createTestTemplate('test-template', [ + it('includes template metadata', function () { + createTestTemplate('test-template', [ 'name' => 'Test Template', 'description' => 'A test description', 'category' => 'testing', @@ -100,18 +83,18 @@ class PlanTemplateServiceTest extends TestCase $result = $this->service->list(); - $this->assertCount(1, $result); - $template = $result[0]; - $this->assertEquals('test-template', $template['slug']); - $this->assertEquals('Test Template', $template['name']); - $this->assertEquals('A test description', $template['description']); - $this->assertEquals('testing', $template['category']); - $this->assertEquals(2, $template['phases_count']); - } + expect($result)->toHaveCount(1); - public function test_list_extracts_variables(): void - { - $this->createTestTemplate('with-vars', [ + $template = $result[0]; + expect($template['slug'])->toBe('test-template') + ->and($template['name'])->toBe('Test Template') + ->and($template['description'])->toBe('A test description') + ->and($template['category'])->toBe('testing') + ->and($template['phases_count'])->toBe(2); + }); + + it('extracts variable definitions', function () { + createTestTemplate('with-vars', [ 'name' => 'Template with Variables', 'variables' => [ 'project_name' => [ @@ -128,44 +111,43 @@ class PlanTemplateServiceTest extends TestCase ]); $result = $this->service->list(); - $template = $result[0]; - $this->assertCount(2, $template['variables']); - $this->assertEquals('project_name', $template['variables'][0]['name']); - $this->assertTrue($template['variables'][0]['required']); - $this->assertEquals('author', $template['variables'][1]['name']); - $this->assertEquals('Anonymous', $template['variables'][1]['default']); - } - public function test_list_ignores_non_yaml_files(): void - { - $this->createTestTemplate('valid-template', ['name' => 'Valid', 'phases' => []]); + expect($template['variables'])->toHaveCount(2) + ->and($template['variables'][0]['name'])->toBe('project_name') + ->and($template['variables'][0]['required'])->toBeTrue() + ->and($template['variables'][1]['name'])->toBe('author') + ->and($template['variables'][1]['default'])->toBe('Anonymous'); + }); + + it('ignores non-YAML files', function () { + createTestTemplate('valid-template', ['name' => 'Valid', 'phases' => []]); File::put($this->testTemplatesPath.'/readme.txt', 'Not a template'); File::put($this->testTemplatesPath.'/config.json', '{}'); $result = $this->service->list(); - $this->assertCount(1, $result); - $this->assertEquals('valid-template', $result[0]['slug']); - } + expect($result)->toHaveCount(1) + ->and($result[0]['slug'])->toBe('valid-template'); + }); - public function test_list_templates_returns_array(): void - { - $this->createTestTemplate('test-template', ['name' => 'Test', 'phases' => []]); + it('returns array from listTemplates method', function () { + createTestTemplate('test-template', ['name' => 'Test', 'phases' => []]); $result = $this->service->listTemplates(); - $this->assertIsArray($result); - $this->assertCount(1, $result); - } + expect($result)->toBeArray() + ->toHaveCount(1); + }); +}); - // ========================================================================= - // Get Template Tests - // ========================================================================= +// ========================================================================= +// Get Template Tests +// ========================================================================= - public function test_get_returns_template_content(): void - { - $this->createTestTemplate('my-template', [ +describe('template retrieval', function () { + it('returns template content by slug', function () { + createTestTemplate('my-template', [ 'name' => 'My Template', 'description' => 'Test description', 'phases' => [ @@ -175,37 +157,36 @@ class PlanTemplateServiceTest extends TestCase $result = $this->service->get('my-template'); - $this->assertNotNull($result); - $this->assertEquals('my-template', $result['slug']); - $this->assertEquals('My Template', $result['name']); - $this->assertEquals('Test description', $result['description']); - } + expect($result)->not->toBeNull() + ->and($result['slug'])->toBe('my-template') + ->and($result['name'])->toBe('My Template') + ->and($result['description'])->toBe('Test description'); + }); - public function test_get_returns_null_for_nonexistent(): void - { + it('returns null for nonexistent template', function () { $result = $this->service->get('nonexistent-template'); - $this->assertNull($result); - } + expect($result)->toBeNull(); + }); - public function test_get_supports_yml_extension(): void - { - $yaml = \Symfony\Component\Yaml\Yaml::dump(['name' => 'YML Template', 'phases' => []], 10); + it('supports .yml extension', function () { + $yaml = Yaml::dump(['name' => 'YML Template', 'phases' => []], 10); File::put($this->testTemplatesPath.'/yml-template.yml', $yaml); $result = $this->service->get('yml-template'); - $this->assertNotNull($result); - $this->assertEquals('YML Template', $result['name']); - } + expect($result)->not->toBeNull() + ->and($result['name'])->toBe('YML Template'); + }); +}); - // ========================================================================= - // Preview Template Tests - // ========================================================================= +// ========================================================================= +// Preview Template Tests +// ========================================================================= - public function test_preview_template_returns_structure(): void - { - $this->createTestTemplate('preview-test', [ +describe('template preview', function () { + it('returns complete preview structure', function () { + createTestTemplate('preview-test', [ 'name' => 'Preview Test', 'description' => 'Testing preview', 'category' => 'test', @@ -217,27 +198,25 @@ class PlanTemplateServiceTest extends TestCase $result = $this->service->previewTemplate('preview-test'); - $this->assertNotNull($result); - $this->assertEquals('preview-test', $result['slug']); - $this->assertEquals('Preview Test', $result['name']); - $this->assertEquals('Testing preview', $result['description']); - $this->assertEquals('test', $result['category']); - $this->assertCount(1, $result['phases']); - $this->assertEquals(1, $result['phases'][0]['order']); - $this->assertEquals('Setup', $result['phases'][0]['name']); - $this->assertCount(2, $result['guidelines']); - } + expect($result)->not->toBeNull() + ->and($result['slug'])->toBe('preview-test') + ->and($result['name'])->toBe('Preview Test') + ->and($result['description'])->toBe('Testing preview') + ->and($result['category'])->toBe('test') + ->and($result['phases'])->toHaveCount(1) + ->and($result['phases'][0]['order'])->toBe(1) + ->and($result['phases'][0]['name'])->toBe('Setup') + ->and($result['guidelines'])->toHaveCount(2); + }); - public function test_preview_template_returns_null_for_nonexistent(): void - { + it('returns null for nonexistent template', function () { $result = $this->service->previewTemplate('nonexistent'); - $this->assertNull($result); - } + expect($result)->toBeNull(); + }); - public function test_preview_template_applies_variables(): void - { - $this->createTestTemplate('var-preview', [ + it('applies variable substitution', function () { + createTestTemplate('var-preview', [ 'name' => '{{ project_name }} Plan', 'description' => 'Plan for {{ project_name }}', 'phases' => [ @@ -250,55 +229,53 @@ class PlanTemplateServiceTest extends TestCase 'task_type' => 'Build feature', ]); - $this->assertStringContainsString('MyProject', $result['name']); - $this->assertStringContainsString('MyProject', $result['description']); - $this->assertStringContainsString('MyProject', $result['phases'][0]['name']); - } + expect($result['name'])->toContain('MyProject') + ->and($result['description'])->toContain('MyProject') + ->and($result['phases'][0]['name'])->toContain('MyProject'); + }); - public function test_preview_includes_applied_variables(): void - { - $this->createTestTemplate('track-vars', [ + it('includes applied variables in response', function () { + createTestTemplate('track-vars', [ 'name' => '{{ name }} Template', 'phases' => [], ]); $result = $this->service->previewTemplate('track-vars', ['name' => 'Test']); - $this->assertArrayHasKey('variables_applied', $result); - $this->assertEquals(['name' => 'Test'], $result['variables_applied']); - } + expect($result)->toHaveKey('variables_applied') + ->and($result['variables_applied'])->toBe(['name' => 'Test']); + }); +}); - // ========================================================================= - // Variable Substitution Tests - // ========================================================================= +// ========================================================================= +// Variable Substitution Tests +// ========================================================================= - public function test_substitutes_simple_variables(): void - { - $this->createTestTemplate('simple-vars', [ +describe('variable substitution', function () { + it('substitutes simple variables', function () { + createTestTemplate('simple-vars', [ 'name' => '{{ project }} Project', 'phases' => [], ]); $result = $this->service->previewTemplate('simple-vars', ['project' => 'Alpha']); - $this->assertEquals('Alpha Project', $result['name']); - } + expect($result['name'])->toBe('Alpha Project'); + }); - public function test_substitutes_variables_with_whitespace(): void - { - $this->createTestTemplate('whitespace-vars', [ + it('handles whitespace in variable placeholders', function () { + createTestTemplate('whitespace-vars', [ 'name' => '{{ project }} Project', 'phases' => [], ]); $result = $this->service->previewTemplate('whitespace-vars', ['project' => 'Beta']); - $this->assertEquals('Beta Project', $result['name']); - } + expect($result['name'])->toBe('Beta Project'); + }); - public function test_applies_default_values(): void - { - $this->createTestTemplate('default-vars', [ + it('applies default values when variable not provided', function () { + createTestTemplate('default-vars', [ 'name' => '{{ project }} by {{ author }}', 'variables' => [ 'project' => ['required' => true], @@ -309,12 +286,11 @@ class PlanTemplateServiceTest extends TestCase $result = $this->service->previewTemplate('default-vars', ['project' => 'Gamma']); - $this->assertEquals('Gamma by Unknown', $result['name']); - } + expect($result['name'])->toBe('Gamma by Unknown'); + }); - public function test_handles_special_characters_in_variables(): void - { - $this->createTestTemplate('special-chars', [ + it('handles special characters in variable values', function () { + createTestTemplate('special-chars', [ 'name' => '{{ title }}', 'description' => '{{ desc }}', 'phases' => [], @@ -325,14 +301,12 @@ class PlanTemplateServiceTest extends TestCase 'desc' => 'Has & "quotes"', ]); - // Should handle without corrupting JSON structure - $this->assertNotNull($result); - $this->assertStringContainsString('quotes', $result['name']); - } + expect($result)->not->toBeNull() + ->and($result['name'])->toContain('quotes'); + }); - public function test_ignores_non_scalar_variable_values(): void - { - $this->createTestTemplate('scalar-only', [ + it('ignores non-scalar variable values', function () { + createTestTemplate('scalar-only', [ 'name' => '{{ project }}', 'phases' => [], ]); @@ -341,17 +315,41 @@ class PlanTemplateServiceTest extends TestCase 'project' => ['array' => 'value'], ]); - // Variable should not be substituted - $this->assertStringContainsString('{{ project }}', $result['name']); - } + expect($result['name'])->toContain('{{ project }}'); + }); - // ========================================================================= - // Create Plan Tests - // ========================================================================= + it('handles multiple occurrences of same variable', function () { + createTestTemplate('multi-occurrence', [ + 'name' => '{{ app }} - {{ app }}', + 'description' => 'This is {{ app }}', + 'phases' => [], + ]); - public function test_create_plan_from_template(): void - { - $this->createTestTemplate('create-test', [ + $result = $this->service->previewTemplate('multi-occurrence', ['app' => 'TestApp']); + + expect($result['name'])->toBe('TestApp - TestApp') + ->and($result['description'])->toBe('This is TestApp'); + }); + + it('preserves unsubstituted variables when value not provided', function () { + createTestTemplate('unsubstituted', [ + 'name' => '{{ provided }} and {{ missing }}', + 'phases' => [], + ]); + + $result = $this->service->previewTemplate('unsubstituted', ['provided' => 'Here']); + + expect($result['name'])->toBe('Here and {{ missing }}'); + }); +}); + +// ========================================================================= +// Create Plan Tests +// ========================================================================= + +describe('plan creation from template', function () { + it('creates plan with correct attributes', function () { + createTestTemplate('create-test', [ 'name' => 'Test Template', 'description' => 'Template description', 'phases' => [ @@ -362,24 +360,22 @@ class PlanTemplateServiceTest extends TestCase $plan = $this->service->createPlan('create-test', [], [], $this->workspace); - $this->assertNotNull($plan); - $this->assertInstanceOf(AgentPlan::class, $plan); - $this->assertEquals('Test Template', $plan->title); - $this->assertEquals('Template description', $plan->description); - $this->assertEquals($this->workspace->id, $plan->workspace_id); - $this->assertCount(2, $plan->agentPhases); - } + expect($plan)->not->toBeNull() + ->toBeInstanceOf(AgentPlan::class) + ->and($plan->title)->toBe('Test Template') + ->and($plan->description)->toBe('Template description') + ->and($plan->workspace_id)->toBe($this->workspace->id) + ->and($plan->agentPhases)->toHaveCount(2); + }); - public function test_create_plan_returns_null_for_nonexistent_template(): void - { + it('returns null for nonexistent template', function () { $result = $this->service->createPlan('nonexistent', [], [], $this->workspace); - $this->assertNull($result); - } + expect($result)->toBeNull(); + }); - public function test_create_plan_with_custom_title(): void - { - $this->createTestTemplate('custom-title', [ + it('uses custom title when provided', function () { + createTestTemplate('custom-title', [ 'name' => 'Template Name', 'phases' => [], ]); @@ -391,12 +387,11 @@ class PlanTemplateServiceTest extends TestCase $this->workspace ); - $this->assertEquals('My Custom Title', $plan->title); - } + expect($plan->title)->toBe('My Custom Title'); + }); - public function test_create_plan_with_custom_slug(): void - { - $this->createTestTemplate('custom-slug', [ + it('uses custom slug when provided', function () { + createTestTemplate('custom-slug', [ 'name' => 'Template', 'phases' => [], ]); @@ -408,12 +403,11 @@ class PlanTemplateServiceTest extends TestCase $this->workspace ); - $this->assertEquals('my-custom-slug', $plan->slug); - } + expect($plan->slug)->toBe('my-custom-slug'); + }); - public function test_create_plan_applies_variables(): void - { - $this->createTestTemplate('var-plan', [ + it('applies variables to plan content', function () { + createTestTemplate('var-plan', [ 'name' => '{{ project }} Plan', 'description' => 'Plan for {{ project }}', 'phases' => [ @@ -428,14 +422,13 @@ class PlanTemplateServiceTest extends TestCase $this->workspace ); - $this->assertStringContainsString('MyApp', $plan->title); - $this->assertStringContainsString('MyApp', $plan->description); - $this->assertStringContainsString('MyApp', $plan->agentPhases[0]->name); - } + expect($plan->title)->toContain('MyApp') + ->and($plan->description)->toContain('MyApp') + ->and($plan->agentPhases[0]->name)->toContain('MyApp'); + }); - public function test_create_plan_activates_when_requested(): void - { - $this->createTestTemplate('activate-plan', [ + it('activates plan when requested', function () { + createTestTemplate('activate-plan', [ 'name' => 'Activatable', 'phases' => [], ]); @@ -447,24 +440,22 @@ class PlanTemplateServiceTest extends TestCase $this->workspace ); - $this->assertEquals(AgentPlan::STATUS_ACTIVE, $plan->status); - } + expect($plan->status)->toBe(AgentPlan::STATUS_ACTIVE); + }); - public function test_create_plan_defaults_to_draft(): void - { - $this->createTestTemplate('draft-plan', [ + it('defaults to draft status', function () { + createTestTemplate('draft-plan', [ 'name' => 'Draft Plan', 'phases' => [], ]); $plan = $this->service->createPlan('draft-plan', [], [], $this->workspace); - $this->assertEquals(AgentPlan::STATUS_DRAFT, $plan->status); - } + expect($plan->status)->toBe(AgentPlan::STATUS_DRAFT); + }); - public function test_create_plan_stores_template_metadata(): void - { - $this->createTestTemplate('metadata-plan', [ + it('stores template metadata', function () { + createTestTemplate('metadata-plan', [ 'name' => 'Metadata Template', 'phases' => [], ]); @@ -476,14 +467,13 @@ class PlanTemplateServiceTest extends TestCase $this->workspace ); - $this->assertEquals('template', $plan->metadata['source']); - $this->assertEquals('metadata-plan', $plan->metadata['template_slug']); - $this->assertEquals(['var1' => 'value1'], $plan->metadata['variables']); - } + expect($plan->metadata['source'])->toBe('template') + ->and($plan->metadata['template_slug'])->toBe('metadata-plan') + ->and($plan->metadata['variables'])->toBe(['var1' => 'value1']); + }); - public function test_create_plan_creates_phases_in_order(): void - { - $this->createTestTemplate('ordered-phases', [ + it('creates phases in correct order', function () { + createTestTemplate('ordered-phases', [ 'name' => 'Ordered', 'phases' => [ ['name' => 'First'], @@ -494,17 +484,16 @@ class PlanTemplateServiceTest extends TestCase $plan = $this->service->createPlan('ordered-phases', [], [], $this->workspace); - $this->assertEquals(1, $plan->agentPhases[0]->order); - $this->assertEquals('First', $plan->agentPhases[0]->name); - $this->assertEquals(2, $plan->agentPhases[1]->order); - $this->assertEquals('Second', $plan->agentPhases[1]->name); - $this->assertEquals(3, $plan->agentPhases[2]->order); - $this->assertEquals('Third', $plan->agentPhases[2]->name); - } + expect($plan->agentPhases[0]->order)->toBe(1) + ->and($plan->agentPhases[0]->name)->toBe('First') + ->and($plan->agentPhases[1]->order)->toBe(2) + ->and($plan->agentPhases[1]->name)->toBe('Second') + ->and($plan->agentPhases[2]->order)->toBe(3) + ->and($plan->agentPhases[2]->name)->toBe('Third'); + }); - public function test_create_plan_creates_tasks_as_pending(): void - { - $this->createTestTemplate('task-status', [ + it('creates tasks with pending status', function () { + createTestTemplate('task-status', [ 'name' => 'Task Status', 'phases' => [ ['name' => 'Phase', 'tasks' => ['Task 1', 'Task 2']], @@ -512,15 +501,14 @@ class PlanTemplateServiceTest extends TestCase ]); $plan = $this->service->createPlan('task-status', [], [], $this->workspace); - $tasks = $plan->agentPhases[0]->tasks; - $this->assertEquals('pending', $tasks[0]['status']); - $this->assertEquals('pending', $tasks[1]['status']); - } - public function test_create_plan_handles_complex_task_definitions(): void - { - $this->createTestTemplate('complex-tasks', [ + expect($tasks[0]['status'])->toBe('pending') + ->and($tasks[1]['status'])->toBe('pending'); + }); + + it('handles complex task definitions', function () { + createTestTemplate('complex-tasks', [ 'name' => 'Complex Tasks', 'phases' => [ [ @@ -534,16 +522,15 @@ class PlanTemplateServiceTest extends TestCase ]); $plan = $this->service->createPlan('complex-tasks', [], [], $this->workspace); - $tasks = $plan->agentPhases[0]->tasks; - $this->assertEquals('Simple task', $tasks[0]['name']); - $this->assertEquals('Task with metadata', $tasks[1]['name']); - $this->assertEquals('high', $tasks[1]['priority']); - } - public function test_create_plan_with_workspace_id_option(): void - { - $this->createTestTemplate('workspace-id-option', [ + expect($tasks[0]['name'])->toBe('Simple task') + ->and($tasks[1]['name'])->toBe('Task with metadata') + ->and($tasks[1]['priority'])->toBe('high'); + }); + + it('accepts workspace_id via options', function () { + createTestTemplate('workspace-id-option', [ 'name' => 'Test', 'phases' => [], ]); @@ -554,16 +541,28 @@ class PlanTemplateServiceTest extends TestCase ['workspace_id' => $this->workspace->id] ); - $this->assertEquals($this->workspace->id, $plan->workspace_id); - } + expect($plan->workspace_id)->toBe($this->workspace->id); + }); - // ========================================================================= - // Validate Variables Tests - // ========================================================================= + it('creates plan without workspace when none provided', function () { + createTestTemplate('no-workspace', [ + 'name' => 'No Workspace', + 'phases' => [], + ]); - public function test_validate_variables_returns_valid_when_all_provided(): void - { - $this->createTestTemplate('validate-vars', [ + $plan = $this->service->createPlan('no-workspace', [], []); + + expect($plan->workspace_id)->toBeNull(); + }); +}); + +// ========================================================================= +// Variable Validation Tests +// ========================================================================= + +describe('variable validation', function () { + it('returns valid when all required variables provided', function () { + createTestTemplate('validate-vars', [ 'name' => 'Test', 'variables' => [ 'required_var' => ['required' => true], @@ -573,13 +572,12 @@ class PlanTemplateServiceTest extends TestCase $result = $this->service->validateVariables('validate-vars', ['required_var' => 'value']); - $this->assertTrue($result['valid']); - $this->assertEmpty($result['errors']); - } + expect($result['valid'])->toBeTrue() + ->and($result['errors'])->toBeEmpty(); + }); - public function test_validate_variables_returns_error_when_missing_required(): void - { - $this->createTestTemplate('missing-required', [ + it('returns error when required variable missing', function () { + createTestTemplate('missing-required', [ 'name' => 'Test', 'variables' => [ 'required_var' => ['required' => true], @@ -589,14 +587,13 @@ class PlanTemplateServiceTest extends TestCase $result = $this->service->validateVariables('missing-required', []); - $this->assertFalse($result['valid']); - $this->assertNotEmpty($result['errors']); - $this->assertStringContainsString('required_var', $result['errors'][0]); - } + expect($result['valid'])->toBeFalse() + ->and($result['errors'])->not->toBeEmpty() + ->and($result['errors'][0])->toContain('required_var'); + }); - public function test_validate_variables_accepts_default_for_required(): void - { - $this->createTestTemplate('default-required', [ + it('accepts default value for required variable', function () { + createTestTemplate('default-required', [ 'name' => 'Test', 'variables' => [ 'optional_with_default' => ['required' => true, 'default' => 'default value'], @@ -606,66 +603,91 @@ class PlanTemplateServiceTest extends TestCase $result = $this->service->validateVariables('default-required', []); - $this->assertTrue($result['valid']); - } + expect($result['valid'])->toBeTrue(); + }); - public function test_validate_variables_returns_error_for_nonexistent_template(): void - { + it('returns error for nonexistent template', function () { $result = $this->service->validateVariables('nonexistent', []); - $this->assertFalse($result['valid']); - $this->assertStringContainsString('Template not found', $result['errors'][0]); - } + expect($result['valid'])->toBeFalse() + ->and($result['errors'][0])->toContain('Template not found'); + }); - // ========================================================================= - // Category Tests - // ========================================================================= + it('validates multiple required variables', function () { + createTestTemplate('multi-required', [ + 'name' => 'Test', + 'variables' => [ + 'var1' => ['required' => true], + 'var2' => ['required' => true], + 'var3' => ['required' => false], + ], + 'phases' => [], + ]); - public function test_get_by_category_filters_templates(): void - { - $this->createTestTemplate('dev-1', ['name' => 'Dev 1', 'category' => 'development', 'phases' => []]); - $this->createTestTemplate('dev-2', ['name' => 'Dev 2', 'category' => 'development', 'phases' => []]); - $this->createTestTemplate('ops-1', ['name' => 'Ops 1', 'category' => 'operations', 'phases' => []]); + $result = $this->service->validateVariables('multi-required', ['var1' => 'a']); + + expect($result['valid'])->toBeFalse() + ->and($result['errors'])->toHaveCount(1) + ->and($result['errors'][0])->toContain('var2'); + }); +}); + +// ========================================================================= +// Category Tests +// ========================================================================= + +describe('template categories', function () { + it('filters templates by category', function () { + createTestTemplate('dev-1', ['name' => 'Dev 1', 'category' => 'development', 'phases' => []]); + createTestTemplate('dev-2', ['name' => 'Dev 2', 'category' => 'development', 'phases' => []]); + createTestTemplate('ops-1', ['name' => 'Ops 1', 'category' => 'operations', 'phases' => []]); $devTemplates = $this->service->getByCategory('development'); - $this->assertCount(2, $devTemplates); + expect($devTemplates)->toHaveCount(2); foreach ($devTemplates as $template) { - $this->assertEquals('development', $template['category']); + expect($template['category'])->toBe('development'); } - } + }); - public function test_get_categories_returns_unique_categories(): void - { - $this->createTestTemplate('t1', ['name' => 'T1', 'category' => 'alpha', 'phases' => []]); - $this->createTestTemplate('t2', ['name' => 'T2', 'category' => 'beta', 'phases' => []]); - $this->createTestTemplate('t3', ['name' => 'T3', 'category' => 'alpha', 'phases' => []]); + it('returns unique categories sorted alphabetically', function () { + createTestTemplate('t1', ['name' => 'T1', 'category' => 'alpha', 'phases' => []]); + createTestTemplate('t2', ['name' => 'T2', 'category' => 'beta', 'phases' => []]); + createTestTemplate('t3', ['name' => 'T3', 'category' => 'alpha', 'phases' => []]); $categories = $this->service->getCategories(); - $this->assertCount(2, $categories); - $this->assertContains('alpha', $categories->toArray()); - $this->assertContains('beta', $categories->toArray()); - } + expect($categories)->toHaveCount(2) + ->and($categories->toArray())->toContain('alpha') + ->and($categories->toArray())->toContain('beta'); + }); - public function test_get_categories_returns_sorted(): void - { - $this->createTestTemplate('t1', ['name' => 'T1', 'category' => 'zebra', 'phases' => []]); - $this->createTestTemplate('t2', ['name' => 'T2', 'category' => 'alpha', 'phases' => []]); + it('returns categories in sorted order', function () { + createTestTemplate('t1', ['name' => 'T1', 'category' => 'zebra', 'phases' => []]); + createTestTemplate('t2', ['name' => 'T2', 'category' => 'alpha', 'phases' => []]); $categories = $this->service->getCategories(); - $this->assertEquals('alpha', $categories[0]); - $this->assertEquals('zebra', $categories[1]); - } + expect($categories[0])->toBe('alpha') + ->and($categories[1])->toBe('zebra'); + }); - // ========================================================================= - // Context Building Tests - // ========================================================================= + it('returns empty collection when no templates', function () { + File::cleanDirectory($this->testTemplatesPath); - public function test_creates_context_from_template_data(): void - { - $this->createTestTemplate('with-context', [ + $categories = $this->service->getCategories(); + + expect($categories)->toBeEmpty(); + }); +}); + +// ========================================================================= +// Context Building Tests +// ========================================================================= + +describe('context generation', function () { + it('builds context from template data', function () { + createTestTemplate('with-context', [ 'name' => 'Context Test', 'description' => 'Testing context generation', 'guidelines' => ['Guideline 1', 'Guideline 2'], @@ -679,15 +701,14 @@ class PlanTemplateServiceTest extends TestCase $this->workspace ); - $this->assertNotNull($plan->context); - $this->assertStringContainsString('Context Test', $plan->context); - $this->assertStringContainsString('Testing context generation', $plan->context); - $this->assertStringContainsString('Guideline 1', $plan->context); - } + expect($plan->context)->not->toBeNull() + ->toContain('Context Test') + ->toContain('Testing context generation') + ->toContain('Guideline 1'); + }); - public function test_uses_explicit_context_when_provided(): void - { - $this->createTestTemplate('explicit-context', [ + it('uses explicit context when provided in template', function () { + createTestTemplate('explicit-context', [ 'name' => 'Test', 'context' => 'This is the explicit context.', 'phases' => [], @@ -695,6 +716,102 @@ class PlanTemplateServiceTest extends TestCase $plan = $this->service->createPlan('explicit-context', [], [], $this->workspace); - $this->assertEquals('This is the explicit context.', $plan->context); - } -} + expect($plan->context)->toBe('This is the explicit context.'); + }); + + it('includes variables in generated context', function () { + createTestTemplate('vars-in-context', [ + 'name' => 'Variable Context', + 'description' => 'A plan with variables', + 'phases' => [], + ]); + + $plan = $this->service->createPlan( + 'vars-in-context', + ['key1' => 'value1', 'key2' => 'value2'], + [], + $this->workspace + ); + + expect($plan->context) + ->toContain('key1') + ->toContain('value1') + ->toContain('key2') + ->toContain('value2'); + }); +}); + +// ========================================================================= +// Edge Cases and Error Handling +// ========================================================================= + +describe('edge cases', function () { + it('handles empty phases array', function () { + createTestTemplate('no-phases', [ + 'name' => 'No Phases', + 'phases' => [], + ]); + + $plan = $this->service->createPlan('no-phases', [], [], $this->workspace); + + expect($plan->agentPhases)->toBeEmpty(); + }); + + it('handles phases without tasks', function () { + createTestTemplate('no-tasks', [ + 'name' => 'No Tasks', + 'phases' => [ + ['name' => 'Empty Phase'], + ], + ]); + + $plan = $this->service->createPlan('no-tasks', [], [], $this->workspace); + + expect($plan->agentPhases)->toHaveCount(1) + ->and($plan->agentPhases[0]->tasks)->toBeEmpty(); + }); + + it('handles template without description', function () { + createTestTemplate('no-desc', [ + 'name' => 'No Description', + 'phases' => [], + ]); + + $plan = $this->service->createPlan('no-desc', [], [], $this->workspace); + + expect($plan->description)->toBeNull(); + }); + + it('handles template without variables section', function () { + createTestTemplate('no-vars-section', [ + 'name' => 'No Variables', + 'phases' => [], + ]); + + $result = $this->service->validateVariables('no-vars-section', []); + + expect($result['valid'])->toBeTrue(); + }); + + it('handles malformed YAML gracefully', function () { + File::put($this->testTemplatesPath.'/malformed.yaml', "invalid: yaml: content: ["); + + // Should not throw when listing + $result = $this->service->list(); + + // Malformed template may be excluded or cause specific behaviour + expect($result)->toBeInstanceOf(\Illuminate\Support\Collection::class); + }); + + it('generates unique slug for plans with same title', function () { + createTestTemplate('duplicate-title', [ + 'name' => 'Same Title', + 'phases' => [], + ]); + + $plan1 = $this->service->createPlan('duplicate-title', [], [], $this->workspace); + $plan2 = $this->service->createPlan('duplicate-title', [], [], $this->workspace); + + expect($plan1->slug)->not->toBe($plan2->slug); + }); +});