create(); $this->artisan('form-failures:resolve', ['id' => $failure->id, '--note' => 'fixed via console']) ->expectsConfirmation("Resolve failure {$failure->id}?", 'yes') ->expectsOutput("Resolved failure {$failure->id}.") ->assertSuccessful(); $reloaded = FormSubmissionActionFailure::query()->find($failure->id); $this->assertNotNull($reloaded->resolved_at); $this->assertSame('fixed via console', $reloaded->resolved_note); } public function test_resolve_no_op_on_already_resolved(): void { $failure = FormSubmissionActionFailure::factory()->resolved()->create(); $this->artisan('form-failures:resolve', ['id' => $failure->id]) ->expectsOutput("Failure {$failure->id} already resolved at {$failure->resolved_at}; no-op.") ->assertSuccessful(); } public function test_resolve_skips_already_dismissed(): void { $failure = FormSubmissionActionFailure::factory()->dismissed()->create(); $this->artisan('form-failures:resolve', ['id' => $failure->id]) ->expectsOutput("Failure {$failure->id} already dismissed at {$failure->dismissed_at}; cannot resolve.") ->assertSuccessful(); } public function test_dismiss_with_enum_reason(): void { $failure = FormSubmissionActionFailure::factory()->create(); $this->artisan('form-failures:dismiss', [ 'id' => $failure->id, '--reason' => DismissalReasonType::SCHEMA_DELETED->value, ])->assertSuccessful(); $reloaded = FormSubmissionActionFailure::query()->find($failure->id); $this->assertNotNull($reloaded->dismissed_at); $this->assertSame(DismissalReasonType::SCHEMA_DELETED, $reloaded->dismissed_reason_type); } public function test_dismiss_other_without_note_fails(): void { $failure = FormSubmissionActionFailure::factory()->create(); $this->artisan('form-failures:dismiss', [ 'id' => $failure->id, '--reason' => DismissalReasonType::OTHER->value, ])->assertFailed(); } public function test_dismiss_other_with_note_succeeds(): void { $failure = FormSubmissionActionFailure::factory()->create(); $this->artisan('form-failures:dismiss', [ 'id' => $failure->id, '--reason' => DismissalReasonType::OTHER->value, '--note' => 'manual investigation closed it', ])->assertSuccessful(); $reloaded = FormSubmissionActionFailure::query()->find($failure->id); $this->assertSame('manual investigation closed it', $reloaded->dismissed_reason_note); } public function test_retry_dry_run_does_not_modify_state(): void { $failure = FormSubmissionActionFailure::factory()->create(); $this->artisan('form-failures:retry', ['--id' => $failure->id, '--dry-run' => true]) ->assertSuccessful(); $reloaded = FormSubmissionActionFailure::query()->find($failure->id); $this->assertNull($reloaded->resolved_at); $this->assertSame(0, $reloaded->retry_count); } public function test_retry_requires_at_least_one_filter(): void { $this->artisan('form-failures:retry') ->expectsOutput('At least one of --id, --submission, or --org is required.') ->assertFailed(); } public function test_retry_by_org_isolates_to_that_organisation(): void { $orgA = Organisation::factory()->create(); $orgB = Organisation::factory()->create(); $submissionA = FormSubmission::factory()->forOrganisation($orgA)->create(); $submissionB = FormSubmission::factory()->forOrganisation($orgB)->create(); $failureA = FormSubmissionActionFailure::factory() ->for($submissionA, 'submission') ->create(); $failureB = FormSubmissionActionFailure::factory() ->for($submissionB, 'submission') ->create(); // Dry-run isolates risk; we just verify the filter works. $this->artisan('form-failures:retry', ['--org' => $orgA->id, '--dry-run' => true]) ->expectsOutputToContain((string) $failureA->id) ->doesntExpectOutputToContain((string) $failureB->id) ->assertSuccessful(); } }