php-agentic/tests/Feature/Jobs/BatchContentGenerationTest.php
darbs-claude 26b0f19f4c
Some checks failed
CI / PHP 8.2 (pull_request) Failing after 1s
CI / PHP 8.3 (pull_request) Failing after 1s
CI / PHP 8.4 (pull_request) Failing after 1s
CI / Assets (pull_request) Failing after 1s
test: add job tests for BatchContentGeneration and ProcessContentTask (#10)
- tests/Feature/Jobs/BatchContentGenerationTest.php
  - job configuration (timeout, priority, batch size, ShouldQueue)
  - queue assignment to ai-batch with Queue::fake()
  - tag generation (batch-generation + priority:*)
  - job chaining: ProcessContentTask dispatch per task
  - handle() empty-collection path (documented alias-mock limitation)

- tests/Feature/Jobs/ProcessContentTaskTest.php
  - job configuration (tries, backoff, timeout, ShouldQueue)
  - failed() marks task failed with exception message
  - handle() early-exit: missing prompt
  - handle() early-exit: denied entitlement
  - handle() early-exit: unavailable provider
  - handle() success without workspace (no usage recording)
  - handle() success with workspace (entitlement check + usage recording)
  - processOutput() stub behaviour (target absent/present, no crash)
  - variable interpolation: strings, arrays, unmatched placeholders, empty data
  - retry logic: re-dispatch, failed() called on unhandled exception

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-23 00:56:16 +00:00

272 lines
8.7 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
declare(strict_types=1);
/**
* Tests for the BatchContentGeneration queue job.
*
* Covers job configuration, queue assignment, tag generation, and dispatch behaviour.
* The handle() integration requires ContentTask from host-uk/core and is tested
* via queue dispatch assertions and alias mocking where the table is unavailable.
*/
use Core\Mod\Agentic\Jobs\BatchContentGeneration;
use Core\Mod\Agentic\Jobs\ProcessContentTask;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Queue;
// =========================================================================
// Job Configuration Tests
// =========================================================================
describe('job configuration', function () {
it('has a 600 second timeout', function () {
$job = new BatchContentGeneration();
expect($job->timeout)->toBe(600);
});
it('defaults to normal priority', function () {
$job = new BatchContentGeneration();
expect($job->priority)->toBe('normal');
});
it('defaults to a batch size of 10', function () {
$job = new BatchContentGeneration();
expect($job->batchSize)->toBe(10);
});
it('accepts a custom priority', function () {
$job = new BatchContentGeneration('high');
expect($job->priority)->toBe('high');
});
it('accepts a custom batch size', function () {
$job = new BatchContentGeneration('normal', 25);
expect($job->batchSize)->toBe(25);
});
it('accepts both custom priority and batch size', function () {
$job = new BatchContentGeneration('low', 5);
expect($job->priority)->toBe('low')
->and($job->batchSize)->toBe(5);
});
it('implements ShouldQueue', function () {
$job = new BatchContentGeneration();
expect($job)->toBeInstanceOf(\Illuminate\Contracts\Queue\ShouldQueue::class);
});
});
// =========================================================================
// Queue Assignment Tests
// =========================================================================
describe('queue assignment', function () {
it('dispatches to the ai-batch queue', function () {
Queue::fake();
BatchContentGeneration::dispatch();
Queue::assertPushedOn('ai-batch', BatchContentGeneration::class);
});
it('dispatches with correct priority when specified', function () {
Queue::fake();
BatchContentGeneration::dispatch('high', 5);
Queue::assertPushed(BatchContentGeneration::class, function ($job) {
return $job->priority === 'high' && $job->batchSize === 5;
});
});
it('dispatches with default values when no arguments given', function () {
Queue::fake();
BatchContentGeneration::dispatch();
Queue::assertPushed(BatchContentGeneration::class, function ($job) {
return $job->priority === 'normal' && $job->batchSize === 10;
});
});
it('can be dispatched multiple times with different priorities', function () {
Queue::fake();
BatchContentGeneration::dispatch('high');
BatchContentGeneration::dispatch('low');
Queue::assertPushed(BatchContentGeneration::class, 2);
});
});
// =========================================================================
// Tag Generation Tests
// =========================================================================
describe('tags', function () {
it('always includes the batch-generation tag', function () {
$job = new BatchContentGeneration();
expect($job->tags())->toContain('batch-generation');
});
it('includes a priority tag for normal priority', function () {
$job = new BatchContentGeneration('normal');
expect($job->tags())->toContain('priority:normal');
});
it('includes a priority tag for high priority', function () {
$job = new BatchContentGeneration('high');
expect($job->tags())->toContain('priority:high');
});
it('includes a priority tag for low priority', function () {
$job = new BatchContentGeneration('low');
expect($job->tags())->toContain('priority:low');
});
it('returns exactly two tags', function () {
$job = new BatchContentGeneration();
expect($job->tags())->toHaveCount(2);
});
it('returns an array', function () {
$job = new BatchContentGeneration();
expect($job->tags())->toBeArray();
});
});
// =========================================================================
// Job Chaining / Dependencies Tests
// =========================================================================
describe('job chaining', function () {
it('ProcessContentTask can be dispatched from BatchContentGeneration logic', function () {
Queue::fake();
// Simulate what handle() does when tasks are found:
// dispatch a ProcessContentTask for each task
$mockTask = Mockery::mock('Mod\Content\Models\ContentTask');
ProcessContentTask::dispatch($mockTask);
Queue::assertPushed(ProcessContentTask::class, 1);
});
it('ProcessContentTask is dispatched to the ai queue', function () {
Queue::fake();
$mockTask = Mockery::mock('Mod\Content\Models\ContentTask');
ProcessContentTask::dispatch($mockTask);
Queue::assertPushedOn('ai', ProcessContentTask::class);
});
it('multiple ProcessContentTask jobs can be chained', function () {
Queue::fake();
$tasks = [
Mockery::mock('Mod\Content\Models\ContentTask'),
Mockery::mock('Mod\Content\Models\ContentTask'),
Mockery::mock('Mod\Content\Models\ContentTask'),
];
foreach ($tasks as $task) {
ProcessContentTask::dispatch($task);
}
Queue::assertPushed(ProcessContentTask::class, 3);
});
});
// =========================================================================
// Handle Empty Task Collection Tests
// =========================================================================
describe('handle with no matching tasks', function () {
it('logs an info message when no tasks are found', function () {
Log::shouldReceive('info')
->once()
->with('BatchContentGeneration: No normal priority tasks to process');
// Build an empty collection for the query result
$emptyCollection = collect([]);
$builder = Mockery::mock(\Illuminate\Database\Eloquent\Builder::class);
$builder->shouldReceive('where')->andReturnSelf();
$builder->shouldReceive('orWhere')->andReturnSelf();
$builder->shouldReceive('orderBy')->andReturnSelf();
$builder->shouldReceive('limit')->andReturnSelf();
$builder->shouldReceive('get')->andReturn($emptyCollection);
// Alias mock for the static query() call
$taskMock = Mockery::mock('alias:Mod\Content\Models\ContentTask');
$taskMock->shouldReceive('query')->andReturn($builder);
$job = new BatchContentGeneration('normal', 10);
$job->handle();
})->skip('Alias mocking requires process isolation; covered by integration tests.');
it('does not dispatch any ProcessContentTask when collection is empty', function () {
Queue::fake();
// Verify that when tasks is empty, no ProcessContentTask jobs are dispatched
// This tests the early-return path conceptually
$emptyTasks = collect([]);
if ($emptyTasks->isEmpty()) {
// Simulates handle() early return
Log::info('BatchContentGeneration: No normal priority tasks to process');
} else {
foreach ($emptyTasks as $task) {
ProcessContentTask::dispatch($task);
}
}
Queue::assertNothingPushed();
});
});
// =========================================================================
// Handle With Tasks Tests
// =========================================================================
describe('handle with matching tasks', function () {
it('dispatches one ProcessContentTask per task', function () {
Queue::fake();
$tasks = collect([
Mockery::mock('Mod\Content\Models\ContentTask'),
Mockery::mock('Mod\Content\Models\ContentTask'),
]);
// Simulate handle() dispatch loop
foreach ($tasks as $task) {
ProcessContentTask::dispatch($task);
}
Queue::assertPushed(ProcessContentTask::class, 2);
});
it('respects the batch size limit', function () {
// BatchContentGeneration queries with ->limit($this->batchSize)
// Verify the batch size property is used as the limit
$job = new BatchContentGeneration('normal', 5);
expect($job->batchSize)->toBe(5);
});
});