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
|
|
|
|
<?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 () {
|
2026-02-23 06:42:24 +00:00
|
|
|
|
$job = new BatchContentGeneration;
|
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
|
|
|
|
|
|
|
|
|
|
expect($job->timeout)->toBe(600);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('defaults to normal priority', function () {
|
2026-02-23 06:42:24 +00:00
|
|
|
|
$job = new BatchContentGeneration;
|
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
|
|
|
|
|
|
|
|
|
|
expect($job->priority)->toBe('normal');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('defaults to a batch size of 10', function () {
|
2026-02-23 06:42:24 +00:00
|
|
|
|
$job = new BatchContentGeneration;
|
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
|
|
|
|
|
|
|
|
|
|
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 () {
|
2026-02-23 06:42:24 +00:00
|
|
|
|
$job = new BatchContentGeneration;
|
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
|
|
|
|
|
|
|
|
|
|
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 () {
|
2026-02-23 06:42:24 +00:00
|
|
|
|
$job = new BatchContentGeneration;
|
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
|
|
|
|
|
|
|
|
|
|
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 () {
|
2026-02-23 06:42:24 +00:00
|
|
|
|
$job = new BatchContentGeneration;
|
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
|
|
|
|
|
|
|
|
|
|
expect($job->tags())->toHaveCount(2);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('returns an array', function () {
|
2026-02-23 06:42:24 +00:00
|
|
|
|
$job = new BatchContentGeneration;
|
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
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|