131 خطوط
6.6 KiB
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()];
|
|
}
|
|
}
|