* SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * See the LICENSE file in the project root for the full license text. */ declare(strict_types=1); namespace App\Tests\Repositories; use App\Domain\TaskAssignment; use App\Repositories\SprintRepository; use App\Repositories\SprintWorkerRepository; use App\Repositories\TaskAssignmentRepository; use App\Repositories\TaskRepository; use App\Repositories\WorkerRepository; use App\Tests\TestCase; use InvalidArgumentException; use PDO; /** * Phase 18: per-cell status on task_assignments. Verifies upsertStatus * semantics, the no-op rule, the "row created with days=0 when status is * set on an empty cell" path, and the new statusGridForSprint reader. */ final class TaskAssignmentRepositoryTest extends TestCase { /** @return array{PDO,TaskAssignmentRepository,int,int,int,int} pdo, repo, sprintId, swId, taskId, workerId */ private function seed(): array { $pdo = $this->makeDb(); $sprints = new SprintRepository($pdo); $workers = new WorkerRepository($pdo); $sw = new SprintWorkerRepository($pdo); $tasks = new TaskRepository($pdo); $repo = new TaskAssignmentRepository($pdo); $sprint = $sprints->create('S', '2026-01-05', '2026-01-30', 0.2); $sprints->materializeWeeks($sprint->id, '2026-01-05', '2026-01-30', 4); $worker = $workers->create('Alice', true, 0.0); $sworker = $sw->add($sprint->id, $worker->id, 0.0); $task = $tasks->create($sprint->id, 'Build thing', null, 1); return [$pdo, $repo, $sprint->id, $sworker->id, $task->id, $worker->id]; } public function testUpsertStatusOnEmptyCellWithDefaultIsNoop(): void { [, $repo, , $swId, $taskId] = $this->seed(); $r = $repo->upsertStatus($taskId, $swId, TaskAssignment::STATUS_ZUGEWIESEN); $this->assertSame('NOOP', $r['action']); $this->assertNull($r['before']); $this->assertNull($r['after']); $this->assertNull($repo->find($taskId, $swId)); } public function testUpsertStatusOnEmptyCellWithExplicitCreatesZeroDayRow(): void { [, $repo, , $swId, $taskId] = $this->seed(); $r = $repo->upsertStatus($taskId, $swId, TaskAssignment::STATUS_GESTARTET); $this->assertSame('CREATE', $r['action']); $this->assertNull($r['before']); $this->assertNotNull($r['after']); $this->assertSame(0.0, $r['after']->days); $this->assertSame(TaskAssignment::STATUS_GESTARTET, $r['after']->status); // Reload to confirm persistence. $loaded = $repo->find($taskId, $swId); $this->assertNotNull($loaded); $this->assertSame(TaskAssignment::STATUS_GESTARTET, $loaded->status); } public function testUpsertStatusUnchangedIsNoop(): void { [, $repo, , $swId, $taskId] = $this->seed(); $repo->upsert($taskId, $swId, 1.5); $first = $repo->upsertStatus($taskId, $swId, TaskAssignment::STATUS_GESTARTET); $this->assertSame('UPDATE', $first['action']); $again = $repo->upsertStatus($taskId, $swId, TaskAssignment::STATUS_GESTARTET); $this->assertSame('NOOP', $again['action']); $this->assertSame(TaskAssignment::STATUS_GESTARTET, $again['after']->status); } public function testUpsertStatusUpdatesExistingRowAndKeepsDays(): void { [, $repo, , $swId, $taskId] = $this->seed(); $repo->upsert($taskId, $swId, 2.5); $r = $repo->upsertStatus($taskId, $swId, TaskAssignment::STATUS_ABGESCHLOSSEN); $this->assertSame('UPDATE', $r['action']); $this->assertSame(2.5, $r['before']->days); $this->assertSame(TaskAssignment::STATUS_ZUGEWIESEN, $r['before']->status); $this->assertSame(2.5, $r['after']->days); $this->assertSame(TaskAssignment::STATUS_ABGESCHLOSSEN, $r['after']->status); } public function testUpsertDaysOnExistingPreservesStatus(): void { [, $repo, , $swId, $taskId] = $this->seed(); $repo->upsert($taskId, $swId, 1.0); $repo->upsertStatus($taskId, $swId, TaskAssignment::STATUS_GESTARTET); $r = $repo->upsert($taskId, $swId, 3.0); $this->assertSame('UPDATE', $r['action']); $this->assertSame(TaskAssignment::STATUS_GESTARTET, $r['after']->status); } public function testUpsertStatusRejectsUnknownValue(): void { [, $repo, , $swId, $taskId] = $this->seed(); $this->expectException(InvalidArgumentException::class); $repo->upsertStatus($taskId, $swId, 'in-flight'); } public function testStatusGridForSprintGroupsByTaskAndWorker(): void { [, $repo, $sprintId, $swId, $taskId] = $this->seed(); $repo->upsert($taskId, $swId, 1.0); $repo->upsertStatus($taskId, $swId, TaskAssignment::STATUS_ABGEBROCHEN); $grid = $repo->statusGridForSprint($sprintId); $this->assertArrayHasKey($taskId, $grid); $this->assertArrayHasKey($swId, $grid[$taskId]); $this->assertSame(TaskAssignment::STATUS_ABGEBROCHEN, $grid[$taskId][$swId]); } public function testHydrateOnFreshUpsertCarriesDefaultStatus(): void { [, $repo, , $swId, $taskId] = $this->seed(); $r = $repo->upsert($taskId, $swId, 1.0); $this->assertSame('CREATE', $r['action']); $this->assertSame(TaskAssignment::STATUS_ZUGEWIESEN, $r['after']->status); $loaded = $repo->find($taskId, $swId); $this->assertNotNull($loaded); $this->assertSame(TaskAssignment::STATUS_ZUGEWIESEN, $loaded->status); } }