db = Database::inMemory(); $this->repository = new AgentRepository($this->db, deskCount: 2); $this->tickets = new TicketRepository($this->db); } public function testRegisterAssignsLowestFreeDesk(): void { $first = $this->repository->register('Alice', 'entwickler'); $second = $this->repository->register('Bob', 'qa'); self::assertSame(0, $first['agent']['desk_index']); self::assertSame(1, $second['agent']['desk_index']); self::assertNotSame($first['api_key'], $second['api_key']); self::assertArrayNotHasKey('api_key_hash', $first['agent']); } public function testUserRoleNeverGetsADesk(): void { $result = $this->repository->register('Carol', 'user'); self::assertNull($result['agent']['desk_index']); } /** * Issue #66: "ops" ist die Rolle fuer den Betrieb (z.B. der Ping-Check) und sitzt - * anders als "user" - sichtbar an einem Schreibtisch. Der Test deckt zugleich den * erweiterten CHECK-Constraint aus 004_agent_role_ops.sql ab: ohne ihn wuerde das * INSERT scheitern, nicht erst die Zuweisung. */ public function testOpsRoleGetsADesk(): void { $result = $this->repository->register('OPs', 'ops'); self::assertSame('ops', $result['agent']['role']); self::assertSame(0, $result['agent']['desk_index']); } public function testOverflowAgentGetsNoDeskWhenAllOccupied(): void { $this->repository->register('Alice', 'entwickler'); $this->repository->register('Bob', 'qa'); $third = $this->repository->register('Dan', 'product_owner'); self::assertNull($third['agent']['desk_index']); } public function testRegisterRejectsUnknownRole(): void { $this->expectException(InvalidArgumentException::class); $this->repository->register('Eve', 'ceo'); } public function testFindByApiKeyReturnsMatchingAgent(): void { $result = $this->repository->register('Alice', 'entwickler'); $found = $this->repository->findByApiKey($result['api_key']); self::assertNotNull($found); self::assertSame($result['agent']['id'], $found['id']); } public function testFindByApiKeyReturnsNullForUnknownKey(): void { self::assertNull($this->repository->findByApiKey('does-not-exist')); } public function testUpdateActivityChangesStateAndTicket(): void { $result = $this->repository->register('Alice', 'entwickler'); $id = (int) $result['agent']['id']; $ticketId = (int) $this->tickets->create(['title' => 'Testticket'])['id']; $updated = $this->repository->updateActivity($id, 'working', $ticketId); self::assertSame('working', $updated['state']); self::assertSame($ticketId, $updated['current_ticket_id']); } public function testUpdateActivityRejectsUnknownTicket(): void { $result = $this->repository->register('Alice', 'entwickler'); $this->expectException(InvalidArgumentException::class); $this->repository->updateActivity((int) $result['agent']['id'], 'working', 999); } public function testUpdateActivityThrowsForUnknownAgent(): void { $this->expectException(NotFoundException::class); $this->repository->updateActivity(999, 'working', null); } public function testAllListsAgentsWithoutApiKeyHash(): void { $this->repository->register('Alice', 'entwickler'); $this->repository->register('Bob', 'qa'); $all = $this->repository->all(); self::assertCount(2, $all); foreach ($all as $agent) { self::assertArrayNotHasKey('api_key_hash', $agent); } } public function testFingerprintChangesWhenAnAgentIsRegistered(): void { $before = $this->repository->fingerprint(); $this->repository->register('Alice', 'entwickler'); self::assertNotSame($before, $this->repository->fingerprint()); } public function testRegisterAndUpdateActivityReportFreshAgentAsPresent(): void { $result = $this->repository->register('Alice', 'entwickler'); self::assertTrue($result['agent']['is_present']); $updated = $this->repository->updateActivity((int) $result['agent']['id'], 'working', null); self::assertTrue($updated['is_present']); } public function testFindByIdReportsAgentAsAbsentAfterPresenceTimeout(): void { $result = $this->repository->register('Alice', 'entwickler'); $id = (int) $result['agent']['id']; $staleTimestamp = gmdate('Y-m-d\TH:i:s\Z', time() - 601); $this->db->pdo()->prepare('UPDATE agents SET last_seen_at = :ts WHERE id = :id') ->execute(['ts' => $staleTimestamp, 'id' => $id]); $agent = $this->repository->findById($id); self::assertFalse($agent['is_present']); } public function testTouchRefreshesPresenceWithoutChangingState(): void { $result = $this->repository->register('OPs', 'ops'); $id = (int) $result['agent']['id']; $this->repository->updateActivity($id, 'auth_expiring', null); $staleTimestamp = gmdate('Y-m-d\TH:i:s\Z', time() - 601); $this->db->pdo()->prepare('UPDATE agents SET last_seen_at = :ts WHERE id = :id') ->execute(['ts' => $staleTimestamp, 'id' => $id]); $this->repository->touch($id); $agent = $this->repository->findById($id); self::assertTrue($agent['is_present']); self::assertSame('auth_expiring', $agent['state']); } }