Files
City10/tests/Feature/CityPresenceTest.php
T
marcocosta10000 e7498e1566 feat(city): add airports, ATC agent, luminous signs, and AI model fallback chain
- Add two airports (north/south) with full geometry: runways, taxiways,
  aprons, terminals, hangars, stands, and helipads; walkable airport
  grounds extend beyond city bounds.
- Introduce ATC AI agent ("Torre de Controle City10") using AI_ATC_MODEL
  to generate takeoff clearances; includes dispatch endpoint with
  30-second cooldown and deterministic fallback on failure.
- Implement AI model fallback chain: preferred model → AI_FALLBACK_MODEL
  before failing, applied in ChatController and PersonTalkController.
- Add luminous sign labels (letreiros) for plazas, beach, food, hospital,
  school, city hall, and airports with day/night glow.
- Fix city presence cache: compute is_me per viewer instead of baking
  into shared per-second cache to prevent cross-user flag leakage.
- Add tests: AirportTest, CityPresenceTest, SignLabelsTest, and
  PersonTalkTest fallback model retry scenario.
2026-10-31 22:06:18 -03:00

89 lines
2.5 KiB
PHP

<?php
namespace Tests\Feature;
use App\Models\Character;
use App\Models\User;
use Database\Seeders\CityWorldSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
/**
* Presence feed correctness: the per-second rows are shared, but the
* is_me flag must always reflect the viewer of the current request —
* otherwise a citizen sees their own character as a remote player and
* the bump-to-talk chat opens with themselves.
*/
class CityPresenceTest extends TestCase
{
use RefreshDatabase;
/**
* Boot world fixtures.
*/
protected function setUp(): void
{
parent::setUp();
$this->seed(CityWorldSeeder::class);
}
/**
* Two viewers polling within the same second each get their own
* is_me flags from the shared cache.
*/
public function test_is_me_is_per_viewer_with_shared_cache(): void
{
[$userA, $charA] = $this->makeCitizen();
[$userB, $charB] = $this->makeCitizen();
// Viewer A seeds the shared per-second cache first.
$first = $this->actingAs($userA)
->getJson('/city/presence')
->assertOk()
->json('citizens');
$this->assertTrue($this->flagFor($first, $charA->id), 'A must see itself as me');
$this->assertFalse($this->flagFor($first, $charB->id), 'A must not see B as me');
// Viewer B hits the SAME cache entry moments later.
$second = $this->actingAs($userB)
->getJson('/city/presence')
->assertOk()
->json('citizens');
$this->assertTrue($this->flagFor($second, $charB->id), 'B must see itself as me');
$this->assertFalse($this->flagFor($second, $charA->id), 'B must not see A as me');
}
/**
* Create an active user paired with its in-world character.
*
* @return array{0: User, 1: Character}
*/
private function makeCitizen(): array
{
$user = User::factory()->create([
'status' => User::STATUS_ACTIVE,
'is_active' => true,
]);
return [$user, Character::ensureForUser($user)];
}
/**
* Extract the is_me flag for a character id from a presence payload.
*
* @param list<array<string, mixed>> $citizens
*/
private function flagFor(array $citizens, int $characterId): bool
{
foreach ($citizens as $c) {
if ((int) $c['id'] === $characterId) {
return (bool) $c['is_me'];
}
}
$this->fail("Character {$characterId} missing from presence payload");
}
}