New_Micro_Learning/backend/tests/Feature/Evidence/EvidenceGenerationTest.php

131 خطوط
6.6 KiB
PHP

<?php
namespace Tests\Feature\Evidence;
use App\Models\User;
use App\Modules\Assessments\Domain\Assessment;
use App\Modules\Assessments\Domain\AssessmentAttempt;
use App\Modules\Assessments\Domain\Question;
use App\Modules\Assessments\Domain\QuestionResult;
use App\Modules\Courses\Domain\Course;
use App\Modules\Courses\Domain\CourseModule;
use App\Modules\Courses\Domain\CourseVersion;
use App\Modules\Courses\Domain\Enums\CourseVersionStatus;
use App\Modules\Courses\Domain\Lesson;
use App\Modules\Evidence\Application\EvidenceGenerationService;
use App\Modules\Evidence\Domain\Events\AssessmentEvidenceCreated;
use App\Modules\Identity\Domain\Enums\UserRole;
use App\Modules\Taxonomy\Domain\ContentTaxonomyMapping;
use App\Modules\Taxonomy\Domain\Enums\MappableType;
use App\Modules\Taxonomy\Domain\Enums\MappingConfirmationStatus;
use App\Modules\Taxonomy\Domain\Enums\MappingSource;
use App\Modules\Taxonomy\Domain\Enums\MappingType;
use App\Modules\Taxonomy\Domain\Enums\TaxonomyStatus;
use App\Modules\Taxonomy\Domain\TaxonomyNode;
use App\Modules\Taxonomy\Domain\TaxonomyType;
use DomainException;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Event;
use Tests\TestCase;
class EvidenceGenerationTest extends TestCase
{
use RefreshDatabase;
public function test_question_result_generates_weighted_traceable_evidence_idempotently(): void
{
Event::fake([AssessmentEvidenceCreated::class]);
[$result, $nodes] = $this->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()];
}
}