## Summary - align the system-panel Operations, Failed operations, and Stuck operations pages to the read-only registry contract by removing inline row triage and keeping row-click inspection - keep retry, cancel, and mark-investigated behavior on the canonical system operation detail page while adding the explicit `Show all operations` return path and updated `Operations / Operation` copy - add and update focused Pest and Livewire coverage for list CTA behavior, detail-owned triage, and view-only versus manage-capable platform access - add Spec 170 implementation artifacts plus the follow-on Spec 171 and Spec 172 packages ## Testing - `vendor/bin/sail artisan test --compact tests/Feature/System/Spec114/OpsTriageActionsTest.php` - `vendor/bin/sail artisan test --compact tests/Feature/Guards/ActionSurfaceContractTest.php` - `vendor/bin/sail artisan test --compact tests/Feature/System/Spec114/OpsFailuresViewTest.php` - `vendor/bin/sail artisan test --compact tests/Feature/System/Spec114/OpsStuckViewTest.php` - integrated browser smoke on `/system/ops/runs`, `/system/ops/failures`, `/system/ops/stuck`, empty states via search filter, and detail-page retry confirmation visibility ## Notes - branch pushed from `170-system-operations-surface-alignment` - latest commit: `64b4d741 feat: align system operations surfaces` Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #201
318 lines
12 KiB
PHP
318 lines
12 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Filament\System\Pages\Ops\Failures;
|
|
use App\Filament\System\Pages\Ops\Runbooks;
|
|
use App\Filament\System\Pages\Ops\Runs;
|
|
use App\Filament\System\Pages\Ops\Stuck;
|
|
use App\Filament\System\Pages\Ops\ViewRun;
|
|
use App\Models\AuditLog;
|
|
use App\Models\OperationRun;
|
|
use App\Models\PlatformUser;
|
|
use App\Models\Tenant;
|
|
use App\Support\Auth\PlatformCapabilities;
|
|
use App\Support\OperationRunOutcome;
|
|
use App\Support\OperationRunStatus;
|
|
use App\Support\System\SystemOperationRunLinks;
|
|
use Carbon\CarbonImmutable;
|
|
use Filament\Actions\Action;
|
|
use Filament\Facades\Filament;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Notifications\DatabaseNotification;
|
|
use Illuminate\Support\Facades\Notification as NotificationFacade;
|
|
use Livewire\Livewire;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
beforeEach(function () {
|
|
Filament::setCurrentPanel('system');
|
|
Filament::bootCurrentPanel();
|
|
|
|
Tenant::factory()->create([
|
|
'tenant_id' => null,
|
|
'external_id' => 'platform',
|
|
]);
|
|
});
|
|
|
|
afterEach(function () {
|
|
CarbonImmutable::setTestNow();
|
|
});
|
|
|
|
it('keeps the operations list scan-first with one go to runbooks cta', function () {
|
|
$run = OperationRun::factory()->create([
|
|
'status' => OperationRunStatus::Completed->value,
|
|
'outcome' => OperationRunOutcome::Failed->value,
|
|
'type' => 'inventory_sync',
|
|
]);
|
|
|
|
$viewOnlyUser = PlatformUser::factory()->create([
|
|
'capabilities' => [
|
|
PlatformCapabilities::ACCESS_SYSTEM_PANEL,
|
|
PlatformCapabilities::OPERATIONS_VIEW,
|
|
],
|
|
'is_active' => true,
|
|
]);
|
|
|
|
$this->actingAs($viewOnlyUser, 'platform');
|
|
|
|
$livewire = Livewire::test(Runs::class)
|
|
->assertCanSeeTableRecords([$run])
|
|
->assertActionVisible('go_to_runbooks')
|
|
->assertActionExists('go_to_runbooks', fn (Action $action): bool => $action->getLabel() === 'Go to runbooks' && $action->getUrl() === Runbooks::getUrl(panel: 'system'));
|
|
|
|
$table = $livewire->instance()->getTable();
|
|
$emptyStateActions = collect($table->getEmptyStateActions())
|
|
->map(fn (Action $action): array => [
|
|
'name' => $action->getName(),
|
|
'label' => $action->getLabel(),
|
|
'url' => $action->getUrl(),
|
|
])
|
|
->values()
|
|
->all();
|
|
|
|
expect($livewire->instance()->getTitle())->toBe('Operations')
|
|
->and($table->getActions())->toBeEmpty()
|
|
->and($table->getBulkActions())->toBeEmpty()
|
|
->and($table->getRecordUrl($run))->toBe(SystemOperationRunLinks::view($run))
|
|
->and($emptyStateActions)->toBe([
|
|
[
|
|
'name' => 'go_to_runbooks_empty',
|
|
'label' => 'Go to runbooks',
|
|
'url' => Runbooks::getUrl(panel: 'system'),
|
|
],
|
|
]);
|
|
});
|
|
|
|
it('keeps the failed operations list scan-first with one show all operations cta', function () {
|
|
$run = OperationRun::factory()->create([
|
|
'status' => OperationRunStatus::Completed->value,
|
|
'outcome' => OperationRunOutcome::Failed->value,
|
|
'type' => 'inventory_sync',
|
|
]);
|
|
|
|
$viewOnlyUser = PlatformUser::factory()->create([
|
|
'capabilities' => [
|
|
PlatformCapabilities::ACCESS_SYSTEM_PANEL,
|
|
PlatformCapabilities::OPERATIONS_VIEW,
|
|
],
|
|
'is_active' => true,
|
|
]);
|
|
|
|
$this->actingAs($viewOnlyUser, 'platform');
|
|
|
|
$livewire = Livewire::test(Failures::class)
|
|
->assertCanSeeTableRecords([$run])
|
|
->assertActionVisible('show_all_operations')
|
|
->assertActionExists('show_all_operations', fn (Action $action): bool => $action->getLabel() === 'Show all operations' && $action->getUrl() === SystemOperationRunLinks::index());
|
|
|
|
$table = $livewire->instance()->getTable();
|
|
$emptyStateActions = collect($table->getEmptyStateActions())
|
|
->map(fn (Action $action): array => [
|
|
'name' => $action->getName(),
|
|
'label' => $action->getLabel(),
|
|
'url' => $action->getUrl(),
|
|
])
|
|
->values()
|
|
->all();
|
|
|
|
expect($livewire->instance()->getTitle())->toBe('Failed operations')
|
|
->and($table->getActions())->toBeEmpty()
|
|
->and($table->getBulkActions())->toBeEmpty()
|
|
->and($table->getRecordUrl($run))->toBe(SystemOperationRunLinks::view($run))
|
|
->and($emptyStateActions)->toBe([
|
|
[
|
|
'name' => 'show_all_operations_empty',
|
|
'label' => 'Show all operations',
|
|
'url' => SystemOperationRunLinks::index(),
|
|
],
|
|
]);
|
|
});
|
|
|
|
it('keeps the stuck operations list scan-first with one show all operations cta', function () {
|
|
config()->set('tenantpilot.system_console.stuck_thresholds.queued_minutes', 10);
|
|
config()->set('tenantpilot.system_console.stuck_thresholds.running_minutes', 20);
|
|
|
|
CarbonImmutable::setTestNow(CarbonImmutable::parse('2026-02-27 10:00:00'));
|
|
|
|
$run = OperationRun::factory()->create([
|
|
'status' => OperationRunStatus::Queued->value,
|
|
'outcome' => OperationRunOutcome::Pending->value,
|
|
'type' => 'inventory_sync',
|
|
'created_at' => now()->subMinutes(30),
|
|
'started_at' => null,
|
|
]);
|
|
|
|
$viewOnlyUser = PlatformUser::factory()->create([
|
|
'capabilities' => [
|
|
PlatformCapabilities::ACCESS_SYSTEM_PANEL,
|
|
PlatformCapabilities::OPERATIONS_VIEW,
|
|
],
|
|
'is_active' => true,
|
|
]);
|
|
|
|
$this->actingAs($viewOnlyUser, 'platform');
|
|
|
|
$livewire = Livewire::test(Stuck::class)
|
|
->assertCanSeeTableRecords([$run])
|
|
->assertActionVisible('show_all_operations')
|
|
->assertActionExists('show_all_operations', fn (Action $action): bool => $action->getLabel() === 'Show all operations' && $action->getUrl() === SystemOperationRunLinks::index());
|
|
|
|
$table = $livewire->instance()->getTable();
|
|
$emptyStateActions = collect($table->getEmptyStateActions())
|
|
->map(fn (Action $action): array => [
|
|
'name' => $action->getName(),
|
|
'label' => $action->getLabel(),
|
|
'url' => $action->getUrl(),
|
|
])
|
|
->values()
|
|
->all();
|
|
|
|
expect($livewire->instance()->getTitle())->toBe('Stuck operations')
|
|
->and($table->getActions())->toBeEmpty()
|
|
->and($table->getBulkActions())->toBeEmpty()
|
|
->and($table->getRecordUrl($run))->toBe(SystemOperationRunLinks::view($run))
|
|
->and($emptyStateActions)->toBe([
|
|
[
|
|
'name' => 'show_all_operations_empty',
|
|
'label' => 'Show all operations',
|
|
'url' => SystemOperationRunLinks::index(),
|
|
],
|
|
]);
|
|
});
|
|
|
|
it('keeps detail-page triage, return paths, and audit behavior for manage operators', function () {
|
|
NotificationFacade::fake();
|
|
|
|
$failedRun = OperationRun::factory()->create([
|
|
'status' => OperationRunStatus::Completed->value,
|
|
'outcome' => OperationRunOutcome::Failed->value,
|
|
'type' => 'inventory_sync',
|
|
]);
|
|
|
|
$runningRun = OperationRun::factory()->create([
|
|
'status' => OperationRunStatus::Running->value,
|
|
'outcome' => OperationRunOutcome::Pending->value,
|
|
'type' => 'inventory_sync',
|
|
'created_at' => now()->subMinutes(15),
|
|
'started_at' => now()->subMinutes(10),
|
|
]);
|
|
|
|
$manageUser = PlatformUser::factory()->create([
|
|
'capabilities' => [
|
|
PlatformCapabilities::ACCESS_SYSTEM_PANEL,
|
|
PlatformCapabilities::OPERATIONS_VIEW,
|
|
PlatformCapabilities::OPERATIONS_MANAGE,
|
|
],
|
|
'is_active' => true,
|
|
]);
|
|
|
|
$this->actingAs($manageUser, 'platform');
|
|
|
|
$failedRunView = Livewire::test(ViewRun::class, [
|
|
'run' => $failedRun,
|
|
])
|
|
->assertActionVisible('show_all_operations')
|
|
->assertActionExists('show_all_operations', fn (Action $action): bool => $action->getLabel() === 'Show all operations' && $action->getUrl() === SystemOperationRunLinks::index())
|
|
->assertActionVisible('go_to_runbooks')
|
|
->assertActionExists('go_to_runbooks', fn (Action $action): bool => $action->getLabel() === 'Go to runbooks' && $action->getUrl() === Runbooks::getUrl(panel: 'system'))
|
|
->assertActionVisible('retry')
|
|
->assertActionExists('retry', fn (Action $action): bool => $action->getLabel() === 'Retry' && $action->isConfirmationRequired())
|
|
->assertActionVisible('mark_investigated')
|
|
->assertActionExists('mark_investigated', fn (Action $action): bool => $action->getLabel() === 'Mark investigated' && $action->isConfirmationRequired())
|
|
->assertActionHidden('cancel');
|
|
|
|
expect($failedRunView->instance()->getTitle())->toBe('Operation #'.(int) $failedRun->getKey());
|
|
|
|
Livewire::test(ViewRun::class, [
|
|
'run' => $failedRun,
|
|
])
|
|
->callAction('retry')
|
|
->assertHasNoActionErrors()
|
|
->assertNotified('Inventory sync queued');
|
|
|
|
NotificationFacade::assertNothingSent();
|
|
expect(DatabaseNotification::query()->count())->toBe(0);
|
|
|
|
$retriedRun = OperationRun::query()
|
|
->whereKeyNot((int) $failedRun->getKey())
|
|
->latest('id')
|
|
->first();
|
|
|
|
expect($retriedRun)->not->toBeNull();
|
|
expect((string) $retriedRun?->status)->toBe(OperationRunStatus::Queued->value);
|
|
expect((int) data_get($retriedRun?->context, 'triage.retry_of_run_id'))->toBe((int) $failedRun->getKey());
|
|
|
|
$this->get(SystemOperationRunLinks::view($retriedRun))
|
|
->assertSuccessful()
|
|
->assertSee('Operation #'.(int) $retriedRun?->getKey())
|
|
->assertSee('Show all operations')
|
|
->assertSee('Go to runbooks');
|
|
|
|
Livewire::test(ViewRun::class, [
|
|
'run' => $failedRun,
|
|
])
|
|
->callAction('mark_investigated', data: [
|
|
'reason' => 'Checked by platform operations',
|
|
])
|
|
->assertHasNoActionErrors()
|
|
->assertNotified('Run marked as investigated');
|
|
|
|
Livewire::test(ViewRun::class, [
|
|
'run' => $runningRun,
|
|
])
|
|
->assertActionVisible('show_all_operations')
|
|
->assertActionVisible('go_to_runbooks')
|
|
->assertActionHidden('retry')
|
|
->assertActionVisible('cancel')
|
|
->assertActionExists('cancel', fn (Action $action): bool => $action->getLabel() === 'Cancel' && $action->isConfirmationRequired())
|
|
->assertActionVisible('mark_investigated')
|
|
->callAction('cancel')
|
|
->assertHasNoActionErrors()
|
|
->assertNotified('Run cancelled');
|
|
|
|
expect(AuditLog::query()->where('action', 'platform.system_console.retry')->exists())->toBeTrue();
|
|
expect(AuditLog::query()->where('action', 'platform.system_console.cancel')->exists())->toBeTrue();
|
|
expect(AuditLog::query()->where('action', 'platform.system_console.mark_investigated')->exists())->toBeTrue();
|
|
|
|
$runningRun->refresh();
|
|
|
|
expect((string) $runningRun->status)->toBe(OperationRunStatus::Completed->value)
|
|
->and((string) $runningRun->outcome)->toBe(OperationRunOutcome::Failed->value);
|
|
});
|
|
|
|
it('keeps detail inspection and navigation available while hiding triage for view-only operators', function () {
|
|
$run = OperationRun::factory()->create([
|
|
'status' => OperationRunStatus::Completed->value,
|
|
'outcome' => OperationRunOutcome::Failed->value,
|
|
'type' => 'inventory_sync',
|
|
]);
|
|
|
|
$viewOnlyUser = PlatformUser::factory()->create([
|
|
'capabilities' => [
|
|
PlatformCapabilities::ACCESS_SYSTEM_PANEL,
|
|
PlatformCapabilities::OPERATIONS_VIEW,
|
|
],
|
|
'is_active' => true,
|
|
]);
|
|
|
|
$this->actingAs($viewOnlyUser, 'platform');
|
|
|
|
Livewire::test(ViewRun::class, [
|
|
'run' => $run,
|
|
])
|
|
->assertActionVisible('show_all_operations')
|
|
->assertActionExists('show_all_operations', fn (Action $action): bool => $action->getLabel() === 'Show all operations' && $action->getUrl() === SystemOperationRunLinks::index())
|
|
->assertActionVisible('go_to_runbooks')
|
|
->assertActionExists('go_to_runbooks', fn (Action $action): bool => $action->getLabel() === 'Go to runbooks' && $action->getUrl() === Runbooks::getUrl(panel: 'system'))
|
|
->assertActionHidden('retry')
|
|
->assertActionHidden('cancel')
|
|
->assertActionHidden('mark_investigated');
|
|
|
|
$this->get(SystemOperationRunLinks::view($run))
|
|
->assertSuccessful()
|
|
->assertSee('Operation #'.(int) $run->getKey())
|
|
->assertSee('Show all operations')
|
|
->assertSee('Go to runbooks');
|
|
});
|