foundation(); $service = app(EvidenceGenerationService::class); $evidence = $service->fromQuestionResult($result); $service->fromQuestionResult($result); $this->assertCount(2, $evidence); $this->assertDatabaseCount('evidence_records', 2); $this->assertEqualsCanonicalizing([0.7, 0.3], $evidence->map(fn ($item) => (float) $item->mapping_weight)->all()); $this->assertSame(0.8, (float) $evidence->first()->strength); $this->assertSame(0.84, (float) $evidence->first()->normalized_value); $this->assertEqualsCanonicalizing($nodes, $evidence->pluck('taxonomy_node_id')->all()); Event::assertDispatchedTimes(AssessmentEvidenceCreated::class, 2); } public function test_unconfirmed_ai_mapping_does_not_generate_evidence(): void { [$result] = $this->foundation(MappingConfirmationStatus::Draft); $this->assertCount(0, app(EvidenceGenerationService::class)->fromQuestionResult($result)); $this->assertDatabaseCount('evidence_records', 0); } public function test_evidence_record_is_immutable(): void { [$result] = $this->foundation(); $evidence = app(EvidenceGenerationService::class)->fromQuestionResult($result)->firstOrFail(); $this->expectException(DomainException::class); $evidence->update(['normalized_value' => 0.1]); } private function foundation(MappingConfirmationStatus $confirmation = MappingConfirmationStatus::Confirmed): array { $designer = User::factory()->create(['role' => UserRole::CourseDesigner]); $learner = User::factory()->for($designer->organization)->create(['role' => UserRole::Learner]); $course = Course::query()->create([ 'organization_id' => $designer->organization_id, 'title' => 'Leadership', 'slug' => 'leadership', 'created_by' => $designer->getKey(), ]); $version = CourseVersion::query()->create([ 'organization_id' => $designer->organization_id, 'course_id' => $course->getKey(), 'version_number' => 1, 'status' => CourseVersionStatus::Draft, 'title' => 'Leadership', ]); $module = CourseModule::query()->create([ 'organization_id' => $designer->organization_id, 'course_version_id' => $version->getKey(), 'title' => 'Communication', 'position' => 1, ]); $lesson = Lesson::query()->create([ 'organization_id' => $designer->organization_id, 'course_version_id' => $version->getKey(), 'course_module_id' => $module->getKey(), 'title' => 'Feedback', 'position' => 1, ]); $assessment = Assessment::query()->create([ 'organization_id' => $designer->organization_id, 'course_version_id' => $version->getKey(), 'lesson_id' => $lesson->getKey(), 'title' => 'Feedback check', ]); $question = Question::query()->create([ 'organization_id' => $designer->organization_id, 'course_version_id' => $version->getKey(), 'assessment_id' => $assessment->getKey(), 'type' => 'scenario', 'prompt' => 'Choose a response', 'configuration' => ['choices' => []], 'position' => 1, ]); $type = TaxonomyType::query()->create([ 'organization_id' => $designer->organization_id, 'key' => 'skill', 'name' => 'Skill', 'status' => TaxonomyStatus::Active, ]); $nodes = collect(['Giving feedback', 'Conflict handling'])->map(fn ($name) => TaxonomyNode::query()->create([ 'organization_id' => $designer->organization_id, 'taxonomy_type_id' => $type->getKey(), 'name' => $name, 'status' => TaxonomyStatus::Active, ])); foreach ($nodes->values() as $index => $node) { ContentTaxonomyMapping::query()->create([ 'organization_id' => $designer->organization_id, 'course_version_id' => $version->getKey(), 'mappable_type' => MappableType::Question, 'mappable_id' => $question->getKey(), 'taxonomy_node_id' => $node->getKey(), 'mapping_type' => MappingType::Assesses, 'weight' => $index === 0 ? 0.7 : 0.3, 'source' => MappingSource::Manual, 'confirmation_status' => $confirmation, 'confirmed_by' => $confirmation === MappingConfirmationStatus::Confirmed ? $designer->getKey() : null, ]); } $attempt = AssessmentAttempt::query()->create([ 'organization_id' => $designer->organization_id, 'learner_id' => $learner->getKey(), 'course_version_id' => $version->getKey(), 'assessment_id' => $assessment->getKey(), 'attempt_number' => 1, 'status' => 'completed', 'score' => 0.84, 'started_at' => now()->subMinute(), 'completed_at' => now(), ]); $result = QuestionResult::query()->create([ 'organization_id' => $designer->organization_id, 'assessment_attempt_id' => $attempt->getKey(), 'question_id' => $question->getKey(), 'raw_value' => ['choice' => 'a'], 'normalized_value' => 0.84, 'is_correct' => true, 'occurred_at' => now(), ]); return [$result, $nodes->pluck('id')->all()]; } }