'base64:'.base64_encode(random_bytes(32))]); } config([ 'agentic.mantis.base_url' => 'https://tasks.example.test', 'agentic.mantis.token' => 'mantis-token-123', ]); Carbon::setTestNow('2026-04-26 12:00:00'); $this->app->make(Kernel::class)->registerCommand( $this->app->make(AgenticDispatchQueueCommand::class), ); }); afterEach(function (): void { Carbon::setTestNow(); }); function dispatchQueueProfile(array $attributes = []): AgentProfile { return AgentProfile::create(array_merge([ 'name' => $attributes['name'] ?? 'dispatch-profile', 'gateway_url' => $attributes['gateway_url'] ?? 'https://gateway.example.test', 'api_key_cipher' => $attributes['api_key_cipher'] ?? 'plain-secret', 'cost_class' => 'C', 'capability_tags' => ['dispatch'], 'quota_headroom_pct' => 100, 'enabled' => true, 'last_dispatched_at' => null, ], $attributes)); } test('AgenticDispatchQueueCommand_handle_Good_queues_up_to_the_default_limit_and_marks_tickets_in_flight', function (): void { Queue::fake(); dispatchQueueProfile([ 'name' => 'core-profile', 'cost_class' => 'A', 'capability_tags' => ['core'], ]); dispatchQueueProfile([ 'name' => 'review-profile', 'cost_class' => 'B', 'capability_tags' => ['review'], 'quota_headroom_pct' => 50, ]); dispatchQueueProfile([ 'name' => 'general-profile', 'cost_class' => 'C', 'capability_tags' => ['dispatch'], ]); Http::fake([ 'https://tasks.example.test/api/rest/issues?status=new' => Http::response([ 'issues' => [ [ 'id' => 900, 'handler' => [ 'id' => 7, 'name' => 'assigned-user', ], 'summary' => 'Already assigned ticket', 'tags' => ['core'], ], [ 'id' => 901, 'notes' => [ [ 'text' => 'Picked up by agentic dispatch queue (profile: stale-profile). Suppressing concurrent dispatch for 5min.', 'created_at' => now()->subMinutes(2)->toIso8601String(), ], ], 'summary' => 'Recently claimed ticket', 'tags' => ['review'], ], [ 'id' => 902, 'severity' => 'critical', 'summary' => 'Core escalation', 'tags' => ['core'], ], [ 'id' => 903, 'severity' => 'major', 'summary' => 'Review queue item', 'tags' => ['review'], ], [ 'id' => 904, 'severity' => 'minor', 'summary' => 'General maintenance', 'tags' => [], ], ], ], 200), 'https://tasks.example.test/api/rest/issues/*/notes' => Http::response([], 201), ]); $this->artisan('agentic:dispatch-queue') ->expectsOutputToContain('Queued 3 Mantis ticket(s); skipped 1 assigned, 1 in-flight, 0 without matching profile; 0 failed.') ->assertSuccessful(); Queue::assertPushedOn('ai', DispatchMantisTicketJob::class); Queue::assertPushed(DispatchMantisTicketJob::class, 3); Queue::assertPushed(DispatchMantisTicketJob::class, fn (DispatchMantisTicketJob $job): bool => $job->ticketId === 902); Queue::assertPushed(DispatchMantisTicketJob::class, fn (DispatchMantisTicketJob $job): bool => $job->ticketId === 903); Queue::assertPushed(DispatchMantisTicketJob::class, fn (DispatchMantisTicketJob $job): bool => $job->ticketId === 904); Queue::assertNotPushed(DispatchMantisTicketJob::class, fn (DispatchMantisTicketJob $job): bool => in_array($job->ticketId, [900, 901], true)); Http::assertSent(fn (Request $request): bool => $request->method() === 'GET' && $request->url() === 'https://tasks.example.test/api/rest/issues?status=new' && $request->hasHeader('Authorization', 'mantis-token-123')); Http::assertSent(fn (Request $request): bool => $request->method() === 'POST' && $request->url() === 'https://tasks.example.test/api/rest/issues/902/notes' && $request['text'] === 'Picked up by agentic dispatch queue (profile: core-profile). Suppressing concurrent dispatch for 5min.'); Http::assertSent(fn (Request $request): bool => $request->method() === 'POST' && $request->url() === 'https://tasks.example.test/api/rest/issues/903/notes' && $request['text'] === 'Picked up by agentic dispatch queue (profile: review-profile). Suppressing concurrent dispatch for 5min.'); Http::assertSent(fn (Request $request): bool => $request->method() === 'POST' && $request->url() === 'https://tasks.example.test/api/rest/issues/904/notes' && $request['text'] === 'Picked up by agentic dispatch queue (profile: general-profile). Suppressing concurrent dispatch for 5min.'); Http::assertSentCount(4); }); test('AgenticDispatchQueueCommand_handle_Bad_rejects_non_positive_limits', function (): void { Queue::fake(); Http::fake(); $this->artisan('agentic:dispatch-queue', ['--limit' => 0]) ->expectsOutput('--limit must be greater than zero.') ->assertExitCode(Command::FAILURE); Queue::assertNothingPushed(); Http::assertNothingSent(); }); test('AgenticDispatchQueueCommand_handle_Ugly_honours_a_custom_limit_after_skips', function (): void { Queue::fake(); dispatchQueueProfile([ 'name' => 'core-profile', 'cost_class' => 'A', 'capability_tags' => ['core'], ]); dispatchQueueProfile([ 'name' => 'general-profile', 'cost_class' => 'C', 'capability_tags' => ['dispatch'], ]); Http::fake([ 'https://tasks.example.test/api/rest/issues?status=new' => Http::response([ 'issues' => [ [ 'id' => 910, 'handler' => [ 'id' => 3, 'name' => 'assigned-user', ], 'summary' => 'Assigned first', 'tags' => ['core'], ], [ 'id' => 911, 'severity' => 'critical', 'summary' => 'First queueable ticket', 'tags' => ['core'], ], [ 'id' => 912, 'severity' => 'minor', 'summary' => 'Would queue next if limit allowed', 'tags' => [], ], ], ], 200), 'https://tasks.example.test/api/rest/issues/*/notes' => Http::response([], 201), ]); $this->artisan('agentic:dispatch-queue', ['--limit' => 1]) ->expectsOutputToContain('Queued 1 Mantis ticket(s); skipped 1 assigned, 0 in-flight, 0 without matching profile; 0 failed.') ->assertSuccessful(); Queue::assertPushed(DispatchMantisTicketJob::class, 1); Queue::assertPushed(DispatchMantisTicketJob::class, fn (DispatchMantisTicketJob $job): bool => $job->ticketId === 911); Queue::assertNotPushed(DispatchMantisTicketJob::class, fn (DispatchMantisTicketJob $job): bool => $job->ticketId === 912); Http::assertSentCount(2); });