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 File::cleanDirectory($this->testTemplatesPath); $result = $this->service->list(); $this->assertTrue($result->isEmpty()); } 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' => []]); $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']); } public function test_list_includes_template_metadata(): void { $this->createTestTemplate('test-template', [ 'name' => 'Test Template', 'description' => 'A test description', 'category' => 'testing', 'phases' => [ ['name' => 'Phase 1', 'tasks' => ['Task 1']], ['name' => 'Phase 2', 'tasks' => ['Task 2']], ], ]); $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']); } public function test_list_extracts_variables(): void { $this->createTestTemplate('with-vars', [ 'name' => 'Template with Variables', 'variables' => [ 'project_name' => [ 'description' => 'The project name', 'required' => true, ], 'author' => [ 'description' => 'Author name', 'default' => 'Anonymous', 'required' => false, ], ], 'phases' => [], ]); $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' => []]); 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']); } public function test_list_templates_returns_array(): void { $this->createTestTemplate('test-template', ['name' => 'Test', 'phases' => []]); $result = $this->service->listTemplates(); $this->assertIsArray($result); $this->assertCount(1, $result); } // ========================================================================= // Get Template Tests // ========================================================================= public function test_get_returns_template_content(): void { $this->createTestTemplate('my-template', [ 'name' => 'My Template', 'description' => 'Test description', 'phases' => [ ['name' => 'Phase 1', 'tasks' => ['Task A']], ], ]); $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']); } public function test_get_returns_null_for_nonexistent(): void { $result = $this->service->get('nonexistent-template'); $this->assertNull($result); } public function test_get_supports_yml_extension(): void { $yaml = \Symfony\Component\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']); } // ========================================================================= // Preview Template Tests // ========================================================================= public function test_preview_template_returns_structure(): void { $this->createTestTemplate('preview-test', [ 'name' => 'Preview Test', 'description' => 'Testing preview', 'category' => 'test', 'phases' => [ ['name' => 'Setup', 'description' => 'Initial setup', 'tasks' => ['Install deps']], ], 'guidelines' => ['Be thorough', 'Test everything'], ]); $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']); } public function test_preview_template_returns_null_for_nonexistent(): void { $result = $this->service->previewTemplate('nonexistent'); $this->assertNull($result); } public function test_preview_template_applies_variables(): void { $this->createTestTemplate('var-preview', [ 'name' => '{{ project_name }} Plan', 'description' => 'Plan for {{ project_name }}', 'phases' => [ ['name' => 'Work on {{ project_name }}', 'tasks' => ['{{ task_type }}']], ], ]); $result = $this->service->previewTemplate('var-preview', [ 'project_name' => 'MyProject', 'task_type' => 'Build feature', ]); $this->assertStringContainsString('MyProject', $result['name']); $this->assertStringContainsString('MyProject', $result['description']); $this->assertStringContainsString('MyProject', $result['phases'][0]['name']); } public function test_preview_includes_applied_variables(): void { $this->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']); } // ========================================================================= // Variable Substitution Tests // ========================================================================= public function test_substitutes_simple_variables(): void { $this->createTestTemplate('simple-vars', [ 'name' => '{{ project }} Project', 'phases' => [], ]); $result = $this->service->previewTemplate('simple-vars', ['project' => 'Alpha']); $this->assertEquals('Alpha Project', $result['name']); } public function test_substitutes_variables_with_whitespace(): void { $this->createTestTemplate('whitespace-vars', [ 'name' => '{{ project }} Project', 'phases' => [], ]); $result = $this->service->previewTemplate('whitespace-vars', ['project' => 'Beta']); $this->assertEquals('Beta Project', $result['name']); } public function test_applies_default_values(): void { $this->createTestTemplate('default-vars', [ 'name' => '{{ project }} by {{ author }}', 'variables' => [ 'project' => ['required' => true], 'author' => ['default' => 'Unknown'], ], 'phases' => [], ]); $result = $this->service->previewTemplate('default-vars', ['project' => 'Gamma']); $this->assertEquals('Gamma by Unknown', $result['name']); } public function test_handles_special_characters_in_variables(): void { $this->createTestTemplate('special-chars', [ 'name' => '{{ title }}', 'description' => '{{ desc }}', 'phases' => [], ]); $result = $this->service->previewTemplate('special-chars', [ 'title' => 'Test "quotes" and \\backslashes\\', 'desc' => 'Has & "quotes"', ]); // Should handle without corrupting JSON structure $this->assertNotNull($result); $this->assertStringContainsString('quotes', $result['name']); } public function test_ignores_non_scalar_variable_values(): void { $this->createTestTemplate('scalar-only', [ 'name' => '{{ project }}', 'phases' => [], ]); $result = $this->service->previewTemplate('scalar-only', [ 'project' => ['array' => 'value'], ]); // Variable should not be substituted $this->assertStringContainsString('{{ project }}', $result['name']); } // ========================================================================= // Create Plan Tests // ========================================================================= public function test_create_plan_from_template(): void { $this->createTestTemplate('create-test', [ 'name' => 'Test Template', 'description' => 'Template description', 'phases' => [ ['name' => 'Phase 1', 'tasks' => ['Task 1', 'Task 2']], ['name' => 'Phase 2', 'tasks' => ['Task 3']], ], ]); $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); } public function test_create_plan_returns_null_for_nonexistent_template(): void { $result = $this->service->createPlan('nonexistent', [], [], $this->workspace); $this->assertNull($result); } public function test_create_plan_with_custom_title(): void { $this->createTestTemplate('custom-title', [ 'name' => 'Template Name', 'phases' => [], ]); $plan = $this->service->createPlan( 'custom-title', [], ['title' => 'My Custom Title'], $this->workspace ); $this->assertEquals('My Custom Title', $plan->title); } public function test_create_plan_with_custom_slug(): void { $this->createTestTemplate('custom-slug', [ 'name' => 'Template', 'phases' => [], ]); $plan = $this->service->createPlan( 'custom-slug', [], ['slug' => 'my-custom-slug'], $this->workspace ); $this->assertEquals('my-custom-slug', $plan->slug); } public function test_create_plan_applies_variables(): void { $this->createTestTemplate('var-plan', [ 'name' => '{{ project }} Plan', 'description' => 'Plan for {{ project }}', 'phases' => [ ['name' => '{{ project }} Setup', 'tasks' => ['Configure {{ project }}']], ], ]); $plan = $this->service->createPlan( 'var-plan', ['project' => 'MyApp'], [], $this->workspace ); $this->assertStringContainsString('MyApp', $plan->title); $this->assertStringContainsString('MyApp', $plan->description); $this->assertStringContainsString('MyApp', $plan->agentPhases[0]->name); } public function test_create_plan_activates_when_requested(): void { $this->createTestTemplate('activate-plan', [ 'name' => 'Activatable', 'phases' => [], ]); $plan = $this->service->createPlan( 'activate-plan', [], ['activate' => true], $this->workspace ); $this->assertEquals(AgentPlan::STATUS_ACTIVE, $plan->status); } public function test_create_plan_defaults_to_draft(): void { $this->createTestTemplate('draft-plan', [ 'name' => 'Draft Plan', 'phases' => [], ]); $plan = $this->service->createPlan('draft-plan', [], [], $this->workspace); $this->assertEquals(AgentPlan::STATUS_DRAFT, $plan->status); } public function test_create_plan_stores_template_metadata(): void { $this->createTestTemplate('metadata-plan', [ 'name' => 'Metadata Template', 'phases' => [], ]); $plan = $this->service->createPlan( 'metadata-plan', ['var1' => 'value1'], [], $this->workspace ); $this->assertEquals('template', $plan->metadata['source']); $this->assertEquals('metadata-plan', $plan->metadata['template_slug']); $this->assertEquals(['var1' => 'value1'], $plan->metadata['variables']); } public function test_create_plan_creates_phases_in_order(): void { $this->createTestTemplate('ordered-phases', [ 'name' => 'Ordered', 'phases' => [ ['name' => 'First'], ['name' => 'Second'], ['name' => 'Third'], ], ]); $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); } public function test_create_plan_creates_tasks_as_pending(): void { $this->createTestTemplate('task-status', [ 'name' => 'Task Status', 'phases' => [ ['name' => 'Phase', 'tasks' => ['Task 1', 'Task 2']], ], ]); $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', [ 'name' => 'Complex Tasks', 'phases' => [ [ 'name' => 'Phase', 'tasks' => [ ['name' => 'Simple task'], ['name' => 'Task with metadata', 'priority' => 'high'], ], ], ], ]); $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', [ 'name' => 'Test', 'phases' => [], ]); $plan = $this->service->createPlan( 'workspace-id-option', [], ['workspace_id' => $this->workspace->id] ); $this->assertEquals($this->workspace->id, $plan->workspace_id); } // ========================================================================= // Validate Variables Tests // ========================================================================= public function test_validate_variables_returns_valid_when_all_provided(): void { $this->createTestTemplate('validate-vars', [ 'name' => 'Test', 'variables' => [ 'required_var' => ['required' => true], ], 'phases' => [], ]); $result = $this->service->validateVariables('validate-vars', ['required_var' => 'value']); $this->assertTrue($result['valid']); $this->assertEmpty($result['errors']); } public function test_validate_variables_returns_error_when_missing_required(): void { $this->createTestTemplate('missing-required', [ 'name' => 'Test', 'variables' => [ 'required_var' => ['required' => true], ], 'phases' => [], ]); $result = $this->service->validateVariables('missing-required', []); $this->assertFalse($result['valid']); $this->assertNotEmpty($result['errors']); $this->assertStringContainsString('required_var', $result['errors'][0]); } public function test_validate_variables_accepts_default_for_required(): void { $this->createTestTemplate('default-required', [ 'name' => 'Test', 'variables' => [ 'optional_with_default' => ['required' => true, 'default' => 'default value'], ], 'phases' => [], ]); $result = $this->service->validateVariables('default-required', []); $this->assertTrue($result['valid']); } public function test_validate_variables_returns_error_for_nonexistent_template(): void { $result = $this->service->validateVariables('nonexistent', []); $this->assertFalse($result['valid']); $this->assertStringContainsString('Template not found', $result['errors'][0]); } // ========================================================================= // Category Tests // ========================================================================= 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' => []]); $devTemplates = $this->service->getByCategory('development'); $this->assertCount(2, $devTemplates); foreach ($devTemplates as $template) { $this->assertEquals('development', $template['category']); } } 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' => []]); $categories = $this->service->getCategories(); $this->assertCount(2, $categories); $this->assertContains('alpha', $categories->toArray()); $this->assertContains('beta', $categories->toArray()); } public function test_get_categories_returns_sorted(): void { $this->createTestTemplate('t1', ['name' => 'T1', 'category' => 'zebra', 'phases' => []]); $this->createTestTemplate('t2', ['name' => 'T2', 'category' => 'alpha', 'phases' => []]); $categories = $this->service->getCategories(); $this->assertEquals('alpha', $categories[0]); $this->assertEquals('zebra', $categories[1]); } // ========================================================================= // Context Building Tests // ========================================================================= public function test_creates_context_from_template_data(): void { $this->createTestTemplate('with-context', [ 'name' => 'Context Test', 'description' => 'Testing context generation', 'guidelines' => ['Guideline 1', 'Guideline 2'], 'phases' => [], ]); $plan = $this->service->createPlan( 'with-context', ['project' => 'TestProject'], [], $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); } public function test_uses_explicit_context_when_provided(): void { $this->createTestTemplate('explicit-context', [ 'name' => 'Test', 'context' => 'This is the explicit context.', 'phases' => [], ]); $plan = $this->service->createPlan('explicit-context', [], [], $this->workspace); $this->assertEquals('This is the explicit context.', $plan->context); } }