Implements deterministic model resolution (column → env map → pool → default) and adds AI identity fields to characters. Migration backfills existing rows with deterministic defaults. Controller uses resolved model and identity block in system prompts for contextual conversations.
643 lines
23 KiB
PHP
643 lines
23 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Http\Controllers\Web\PersonTalkController;
|
|
use App\Models\AiAgent;
|
|
use App\Models\Character;
|
|
use App\Models\PersonTalk;
|
|
use App\Models\User;
|
|
use Database\Seeders\CityWorldSeeder;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Db;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Illuminate\Support\Str;
|
|
use Tests\TestCase;
|
|
|
|
/**
|
|
* Daily street conversations (bump-to-talk) coverage: registration per
|
|
* day, peer validation, agent replies with multi-day context and PII
|
|
* kept out of upstream payloads.
|
|
*/
|
|
class PersonTalkTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
/**
|
|
* Seed world fixtures.
|
|
*/
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
$this->seed(CityWorldSeeder::class);
|
|
}
|
|
|
|
/**
|
|
* Create an active user paired with its in-world character.
|
|
*/
|
|
private function makeCitizen(): array
|
|
{
|
|
$user = User::factory()->create([
|
|
'status' => User::STATUS_ACTIVE,
|
|
'is_active' => true,
|
|
]);
|
|
|
|
return [$user, Character::ensureForUser($user)];
|
|
}
|
|
|
|
/**
|
|
* Simulate a human actively using the account (fresh session row).
|
|
*/
|
|
private function makePeerLive(User $user): void
|
|
{
|
|
Db::table('sessions')->insert([
|
|
'id' => Str::random(40),
|
|
'user_id' => $user->id,
|
|
'ip_address' => '127.0.0.1',
|
|
'user_agent' => 'phpunit',
|
|
'payload' => '',
|
|
'last_activity' => now()->getTimestamp(),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Character-to-character talks are stored per day and visible from
|
|
* both sides of the conversation.
|
|
*/
|
|
public function test_character_talk_is_registered_per_day_for_both_sides(): void
|
|
{
|
|
[$userA, $me] = $this->makeCitizen();
|
|
[, $them] = $this->makeCitizen();
|
|
|
|
$this->actingAs($userA)
|
|
->postJson('/talk/person', [
|
|
'kind' => 'character',
|
|
'peer' => $them->id,
|
|
'message' => 'Oi, tudo bem?',
|
|
])
|
|
->assertCreated()
|
|
->assertJsonPath('from', 'me')
|
|
->assertJsonPath('talked_on', now()->toDateString());
|
|
|
|
$this->assertDatabaseHas('person_talks', [
|
|
'from_character_id' => $me->id,
|
|
'to_character_id' => $them->id,
|
|
'to_agent_id' => null,
|
|
'body' => 'Oi, tudo bem?',
|
|
]);
|
|
$stored = PersonTalk::query()->where('body', 'Oi, tudo bem?')->firstOrFail();
|
|
$this->assertSame(now()->toDateString(), $stored->talked_on->toDateString());
|
|
}
|
|
|
|
/**
|
|
* The peer reads the same row as an incoming message and can answer
|
|
* back through the same endpoint.
|
|
*/
|
|
public function test_conversation_is_shared_between_both_characters(): void
|
|
{
|
|
[$userA, $me] = $this->makeCitizen();
|
|
[$userB, $them] = $this->makeCitizen();
|
|
|
|
// Both sides driven by live humans → pure relay, no auto-replies.
|
|
$this->makePeerLive($userA);
|
|
$this->makePeerLive($userB);
|
|
|
|
$this->actingAs($userA)
|
|
->postJson('/talk/person', [
|
|
'kind' => 'character',
|
|
'peer' => $them->id,
|
|
'message' => 'Oi, tudo bem?',
|
|
])
|
|
->assertCreated();
|
|
|
|
$this->actingAs($userB)
|
|
->postJson('/talk/person', [
|
|
'kind' => 'character',
|
|
'peer' => $me->id,
|
|
'message' => 'Tudo ótimo!',
|
|
])
|
|
->assertCreated();
|
|
|
|
$this->actingAs($userB)
|
|
->getJson('/talk/person?kind=character&peer='.$me->id)
|
|
->assertOk()
|
|
->assertJsonPath('peer.kind', 'character')
|
|
->assertJsonPath('messages.0.from', 'them')
|
|
->assertJsonPath('messages.0.body', 'Oi, tudo bem?')
|
|
->assertJsonPath('messages.1.from', 'me')
|
|
->assertJsonPath('messages.1.body', 'Tudo ótimo!');
|
|
}
|
|
|
|
/**
|
|
* Peer and payload validation on the send endpoint.
|
|
*/
|
|
public function test_talk_validates_peer_and_message(): void
|
|
{
|
|
[$userA, $me] = $this->makeCitizen();
|
|
|
|
$this->actingAs($userA)
|
|
->postJson('/talk/person', [
|
|
'kind' => 'character',
|
|
'peer' => $me->id,
|
|
'message' => 'oi',
|
|
])
|
|
->assertStatus(422);
|
|
|
|
$this->actingAs($userA)
|
|
->postJson('/talk/person', [
|
|
'kind' => 'alien',
|
|
'peer' => 1,
|
|
'message' => 'oi',
|
|
])
|
|
->assertStatus(422);
|
|
|
|
$this->actingAs($userA)
|
|
->postJson('/talk/person', [
|
|
'kind' => 'character',
|
|
'peer' => 999999,
|
|
'message' => 'oi',
|
|
])
|
|
->assertNotFound();
|
|
|
|
$this->actingAs($userA)
|
|
->postJson('/talk/person', [
|
|
'kind' => 'character',
|
|
'peer' => 999999,
|
|
])
|
|
->assertStatus(422);
|
|
}
|
|
|
|
/**
|
|
* Agent talks store the message, answer after the response and send
|
|
* the multi-day history upstream while keeping PII out (RULES.md 2.2).
|
|
*/
|
|
public function test_agent_talk_replies_using_daily_history_as_context(): void
|
|
{
|
|
config(['ai.openrouter.api_key' => 'test-key']);
|
|
|
|
Http::fake([
|
|
'openrouter.ai/*' => Http::response([
|
|
'choices' => [['message' => ['content' => 'Praça renovada, como combinamos ontem!']]],
|
|
]),
|
|
]);
|
|
|
|
[$userA, $me] = $this->makeCitizen();
|
|
$agent = AiAgent::create(['name' => 'Prefeito', 'type' => 'mayor']);
|
|
|
|
// Yesterday's exchange is part of the persistent context.
|
|
PersonTalk::create([
|
|
'from_character_id' => $me->id,
|
|
'to_agent_id' => $agent->id,
|
|
'body' => 'Ontem conversamos sobre a praça',
|
|
'talked_on' => now()->subDay()->toDateString(),
|
|
]);
|
|
|
|
$this->actingAs($userA)
|
|
->postJson('/talk/person', [
|
|
'kind' => 'agent',
|
|
'peer' => $agent->id,
|
|
'message' => 'E a praça, fica pronta hoje?',
|
|
])
|
|
->assertCreated();
|
|
|
|
// The after-response job ran during the request lifecycle.
|
|
$this->assertDatabaseHas('person_talks', [
|
|
'from_agent_id' => $agent->id,
|
|
'to_character_id' => $me->id,
|
|
'body' => 'Praça renovada, como combinamos ontem!',
|
|
]);
|
|
$reply = PersonTalk::query()->where('body', 'Praça renovada, como combinamos ontem!')->firstOrFail();
|
|
$this->assertSame(now()->toDateString(), $reply->talked_on->toDateString());
|
|
|
|
$agent->refresh();
|
|
$this->assertSame('active', $agent->status);
|
|
$this->assertSame(0, (int) $agent->error_count);
|
|
$this->assertNotNull($agent->last_think_at);
|
|
$this->assertNotEmpty($agent->last_decision);
|
|
|
|
$userEmail = $userA->email;
|
|
$userName = $userA->name;
|
|
Http::assertSent(function ($request) use ($userEmail, $userName): bool {
|
|
$payload = $request->data();
|
|
$encoded = json_encode($payload, JSON_UNESCAPED_UNICODE);
|
|
|
|
// Yesterday's line and today's line reach the model (thread kept).
|
|
$hasHistory = str_contains($encoded, 'Ontem conversamos sobre a praça')
|
|
&& str_contains($encoded, 'E a praça, fica pronta hoje?');
|
|
// No registration data upstream (RULES.md 2.2).
|
|
$leaksPii = str_contains($encoded, $userEmail)
|
|
|| str_contains($encoded, $userName);
|
|
|
|
return $hasHistory && ! $leaksPii;
|
|
});
|
|
|
|
$this->actingAs($userA)
|
|
->getJson('/talk/person?kind=agent&peer='.$agent->id)
|
|
->assertOk()
|
|
->assertJsonPath('peer.kind', 'agent')
|
|
->assertJsonPath('peer.status', 'active')
|
|
->assertJsonPath('thinking', false)
|
|
->assertJsonPath('messages.0.body', 'Ontem conversamos sobre a praça')
|
|
->assertJsonPath('messages.0.from', 'me')
|
|
->assertJsonPath('messages.1.body', 'E a praça, fica pronta hoje?')
|
|
->assertJsonPath('messages.2.body', 'Praça renovada, como combinamos ontem!')
|
|
->assertJsonPath('messages.2.from', 'them')
|
|
->assertJsonPath('messages.2.talked_on', now()->toDateString());
|
|
}
|
|
|
|
/**
|
|
* Without an upstream key the conversation still completes: the
|
|
* agent stores the localized fallback and counts the failure for
|
|
* backoff (RULES.md 2.4).
|
|
*/
|
|
public function test_agent_talk_without_api_key_stores_fallback_reply(): void
|
|
{
|
|
config(['ai.openrouter.api_key' => null]);
|
|
Http::preventStrayRequests();
|
|
|
|
[$userA, $me] = $this->makeCitizen();
|
|
$agent = AiAgent::create(['name' => 'Lojista', 'type' => 'shopkeeper']);
|
|
|
|
$this->actingAs($userA)
|
|
->postJson('/talk/person', [
|
|
'kind' => 'agent',
|
|
'peer' => $agent->id,
|
|
'message' => 'Bom dia!',
|
|
])
|
|
->assertCreated();
|
|
|
|
$this->assertDatabaseHas('person_talks', [
|
|
'from_agent_id' => $agent->id,
|
|
'to_character_id' => $me->id,
|
|
'body' => __('city.chat_error'),
|
|
'is_fallback' => true,
|
|
]);
|
|
$fallback = PersonTalk::query()->where('body', __('city.chat_error'))->firstOrFail();
|
|
$this->assertSame(now()->toDateString(), $fallback->talked_on->toDateString());
|
|
$this->assertTrue((bool) $fallback->is_fallback);
|
|
|
|
$agent->refresh();
|
|
$this->assertSame(1, (int) $agent->error_count);
|
|
|
|
$this->actingAs($userA)
|
|
->getJson('/talk/person?kind=agent&peer='.$agent->id)
|
|
->assertOk()
|
|
->assertJsonPath('thinking', false);
|
|
}
|
|
|
|
/**
|
|
* While the agent reply is still pending the UI is told to keep
|
|
* showing the thinking state.
|
|
*/
|
|
public function test_agent_talk_reports_thinking_until_reply_lands(): void
|
|
{
|
|
[$userA, $me] = $this->makeCitizen();
|
|
$agent = AiAgent::create(['name' => 'Guia', 'type' => 'pedestrian']);
|
|
|
|
// Pending row without dispatching a reply (simulate in-flight).
|
|
PersonTalk::create([
|
|
'from_character_id' => $me->id,
|
|
'to_agent_id' => $agent->id,
|
|
'body' => 'Oi!',
|
|
'talked_on' => now()->toDateString(),
|
|
]);
|
|
|
|
$this->actingAs($userA)
|
|
->getJson('/talk/person?kind=agent&peer='.$agent->id)
|
|
->assertOk()
|
|
->assertJsonPath('thinking', true);
|
|
}
|
|
|
|
/**
|
|
* History spans multiple days so later conversations keep the thread.
|
|
*/
|
|
public function test_history_returns_previous_days_in_order(): void
|
|
{
|
|
[$userA, $me] = $this->makeCitizen();
|
|
[, $them] = $this->makeCitizen();
|
|
|
|
foreach ([
|
|
[now()->subDays(2), 'Primeira conversa'],
|
|
[now()->subDay(), 'Segunda conversa'],
|
|
[now(), 'Terceira conversa'],
|
|
] as [$day, $body]) {
|
|
PersonTalk::create([
|
|
'from_character_id' => $me->id,
|
|
'to_character_id' => $them->id,
|
|
'body' => $body,
|
|
'talked_on' => $day->toDateString(),
|
|
]);
|
|
}
|
|
|
|
$response = $this->actingAs($userA)
|
|
->getJson('/talk/person?kind=character&peer='.$them->id)
|
|
->assertOk();
|
|
|
|
$messages = $response->json('messages');
|
|
$this->assertCount(3, $messages);
|
|
$this->assertSame('Primeira conversa', $messages[0]['body']);
|
|
$this->assertSame(now()->subDays(2)->toDateString(), $messages[0]['talked_on']);
|
|
$this->assertSame('Terceira conversa', $messages[2]['body']);
|
|
$this->assertSame(now()->toDateString(), $messages[2]['talked_on']);
|
|
}
|
|
|
|
/**
|
|
* A character peer without a live human session (seeded citizen,
|
|
* demo account like "Maria") answers through the AI persona, using
|
|
* the daily history as context and keeping PII out of the payload.
|
|
*/
|
|
public function test_silent_citizen_peer_answers_with_ai_reply(): void
|
|
{
|
|
config(['ai.openrouter.api_key' => 'test-key']);
|
|
|
|
Http::fake([
|
|
'openrouter.ai/*' => Http::response([
|
|
'choices' => [['message' => ['content' => 'Oi! Tudo tranquilo na cidade hoje.']]],
|
|
]),
|
|
]);
|
|
|
|
[$userA, $me] = $this->makeCitizen();
|
|
[$userB, $them] = $this->makeCitizen(); // no session → silent peer
|
|
|
|
PersonTalk::create([
|
|
'from_character_id' => $me->id,
|
|
'to_character_id' => $them->id,
|
|
'body' => 'Ontem te vi na praça',
|
|
'talked_on' => now()->subDay()->toDateString(),
|
|
]);
|
|
|
|
$this->actingAs($userA)
|
|
->postJson('/talk/person', [
|
|
'kind' => 'character',
|
|
'peer' => $them->id,
|
|
'message' => 'Oi, tudo bem por aí?',
|
|
])
|
|
->assertCreated();
|
|
|
|
// The silent peer replied during the after-response phase.
|
|
$reply = PersonTalk::query()
|
|
->where('from_character_id', $them->id)
|
|
->where('to_character_id', $me->id)
|
|
->first();
|
|
$this->assertNotNull($reply);
|
|
$this->assertSame('Oi! Tudo tranquilo na cidade hoje.', $reply->body);
|
|
$this->assertSame(now()->toDateString(), $reply->talked_on->toDateString());
|
|
|
|
$peerEmail = $userB->email;
|
|
Http::assertSent(function ($request) use ($peerEmail): bool {
|
|
$encoded = json_encode($request->data(), JSON_UNESCAPED_UNICODE);
|
|
|
|
// Thread kept… and no registration data upstream (RULES.md 2.2).
|
|
return str_contains($encoded, 'Ontem te vi na praça')
|
|
&& str_contains($encoded, 'Oi, tudo bem por aí?')
|
|
&& ! str_contains($encoded, $peerEmail);
|
|
});
|
|
|
|
$this->actingAs($userA)
|
|
->getJson('/talk/person?kind=character&peer='.$them->id)
|
|
->assertOk()
|
|
->assertJsonPath('thinking', false);
|
|
}
|
|
|
|
/**
|
|
* A peer driven by a live human session is relayed as-is and never
|
|
* answered by the AI on their behalf.
|
|
*/
|
|
public function test_live_human_peer_is_not_impersonated(): void
|
|
{
|
|
config(['ai.openrouter.api_key' => 'test-key']);
|
|
Http::preventStrayRequests();
|
|
|
|
[$userA, $me] = $this->makeCitizen();
|
|
[$userB, $them] = $this->makeCitizen();
|
|
$this->makePeerLive($userB);
|
|
|
|
$this->actingAs($userA)
|
|
->postJson('/talk/person', [
|
|
'kind' => 'character',
|
|
'peer' => $them->id,
|
|
'message' => 'Você está aí?',
|
|
])
|
|
->assertCreated();
|
|
|
|
$this->assertSame(0, PersonTalk::query()
|
|
->where('from_character_id', $them->id)
|
|
->where('to_character_id', $me->id)
|
|
->count());
|
|
|
|
$this->actingAs($userA)
|
|
->getJson('/talk/person?kind=character&peer='.$them->id)
|
|
->assertOk()
|
|
->assertJsonPath('thinking', false);
|
|
}
|
|
|
|
/**
|
|
* Without an API key the silent peer still answers with the stored
|
|
* fallback so the chat never hangs waiting.
|
|
*/
|
|
public function test_silent_citizen_falls_back_without_api_key(): void
|
|
{
|
|
config(['ai.openrouter.api_key' => null]);
|
|
Http::preventStrayRequests();
|
|
|
|
[$userA, $me] = $this->makeCitizen();
|
|
[, $them] = $this->makeCitizen();
|
|
|
|
$this->actingAs($userA)
|
|
->postJson('/talk/person', [
|
|
'kind' => 'character',
|
|
'peer' => $them->id,
|
|
'message' => 'Olá?',
|
|
])
|
|
->assertCreated();
|
|
|
|
$this->assertDatabaseHas('person_talks', [
|
|
'from_character_id' => $them->id,
|
|
'to_character_id' => $me->id,
|
|
'body' => __('city.chat_error'),
|
|
'is_fallback' => true,
|
|
]);
|
|
|
|
$this->actingAs($userA)
|
|
->getJson('/talk/person?kind=character&peer='.$them->id)
|
|
->assertOk()
|
|
->assertJsonPath('thinking', false);
|
|
}
|
|
|
|
/**
|
|
* Error-fallback rows stay visible in the chat but are never sent
|
|
* back to the model as dialogue — an apology in the context would
|
|
* skew the persona into a support-bot tone or identity confusion.
|
|
*/
|
|
public function test_fallback_rows_are_excluded_from_model_context(): void
|
|
{
|
|
config(['ai.openrouter.api_key' => 'test-key']);
|
|
|
|
Http::fake([
|
|
'openrouter.ai/*' => Http::response([
|
|
'choices' => [['message' => ['content' => 'Praça fechada para obra, já passa.']]],
|
|
]),
|
|
]);
|
|
|
|
[$userA, $me] = $this->makeCitizen();
|
|
[$userB, $them] = $this->makeCitizen();
|
|
|
|
// Yesterday: a real line plus an error placeholder.
|
|
PersonTalk::create([
|
|
'from_character_id' => $me->id,
|
|
'to_character_id' => $them->id,
|
|
'body' => 'E a praça, abre hoje?',
|
|
'talked_on' => now()->subDay()->toDateString(),
|
|
]);
|
|
PersonTalk::create([
|
|
'from_character_id' => $them->id,
|
|
'to_character_id' => $me->id,
|
|
'body' => __('city.chat_error'),
|
|
'talked_on' => now()->subDay()->toDateString(),
|
|
'is_fallback' => true,
|
|
]);
|
|
|
|
$this->actingAs($userA)
|
|
->postJson('/talk/person', [
|
|
'kind' => 'character',
|
|
'peer' => $them->id,
|
|
'message' => 'E aí, e a obra?',
|
|
])
|
|
->assertCreated();
|
|
|
|
Http::assertSent(function ($request): bool {
|
|
$encoded = json_encode($request->data(), JSON_UNESCAPED_UNICODE);
|
|
|
|
return str_contains($encoded, 'E a praça, abre hoje?')
|
|
&& str_contains($encoded, 'E aí, e a obra?')
|
|
&& ! str_contains($encoded, __('city.chat_error'));
|
|
});
|
|
|
|
// The stored reply is a real answer, not another placeholder.
|
|
$reply = PersonTalk::query()
|
|
->where('from_character_id', $them->id)
|
|
->where('is_fallback', false)
|
|
->orderByDesc('id')
|
|
->first();
|
|
$this->assertNotNull($reply);
|
|
$this->assertSame('Praça fechada para obra, já passa.', $reply->body);
|
|
}
|
|
|
|
/**
|
|
* Model resolution priority: column → env map (id/name) → pool slot,
|
|
* always inside the allow-list (RULES.md 2.1) and stable per id.
|
|
*/
|
|
public function test_model_resolution_priority_and_allow_list(): void
|
|
{
|
|
$mapped = 'openrouter/free';
|
|
$pooled = 'nvidia/nemotron-3.5-lightning:free';
|
|
|
|
config([
|
|
'ai.models' => [$pooled, $mapped],
|
|
'ai.character_models' => [
|
|
'maria' => $mapped,
|
|
'7' => $pooled,
|
|
'bad' => 'not/allowed-model',
|
|
],
|
|
]);
|
|
|
|
// 1) Character column wins over everything.
|
|
$this->assertSame(
|
|
$mapped,
|
|
PersonTalkController::resolveModel($mapped, 'Qualquer', 99)
|
|
);
|
|
|
|
// 2) Env map by name (case-insensitive) and by id.
|
|
$this->assertSame($mapped, PersonTalkController::resolveModel(null, 'Maria', 3));
|
|
$this->assertSame($pooled, PersonTalkController::resolveModel(null, 'Outro', 7));
|
|
|
|
// 3) Non allow-listed entries are ignored → deterministic pool slot.
|
|
$fromPool = PersonTalkController::resolveModel('not/allowed-model', 'Bad', 4);
|
|
$this->assertContains($fromPool, [$pooled, $mapped]);
|
|
$this->assertNotSame('not/allowed-model', $fromPool);
|
|
|
|
// 4) Empty map/pool falls back to the allow-listed default.
|
|
config(['ai.character_models' => [], 'ai.models' => []]);
|
|
$default = config('ai.default_model');
|
|
$this->assertContains($default, config('ai.allowed_models'));
|
|
$this->assertSame($default, PersonTalkController::resolveModel(null, 'X', 1));
|
|
|
|
// 5) Same character always resolves to the same model.
|
|
config(['ai.models' => [$pooled, $mapped]]);
|
|
$stable = PersonTalkController::resolveModel(null, 'Estável', 5);
|
|
$this->assertSame($stable, PersonTalkController::resolveModel(null, 'Estável', 5));
|
|
}
|
|
|
|
/**
|
|
* The identity/personality profile and the resolved model travel in
|
|
* the upstream payload while registration data stays out (RULES.md 2.2).
|
|
*/
|
|
public function test_identity_and_model_flow_into_upstream_payload(): void
|
|
{
|
|
config([
|
|
'ai.openrouter.api_key' => 'test-key',
|
|
'ai.models' => ['liquid/lfm-2.5-2.6b:free'],
|
|
'ai.character_models' => [],
|
|
]);
|
|
|
|
Http::fake([
|
|
'openrouter.ai/*' => Http::response([
|
|
'choices' => [['message' => ['content' => 'Oi! Tudo tranquilo na cidade.']]],
|
|
]),
|
|
]);
|
|
|
|
[$userA, $me] = $this->makeCitizen();
|
|
[$userB, $them] = $this->makeCitizen();
|
|
|
|
$them->update([
|
|
'ai_age' => 34,
|
|
'ai_gender' => 'feminino',
|
|
'ai_personality' => 'Extrovertido e curiosa, adora puxar conversa.',
|
|
'ai_bio' => 'Moradora do centro, vive nas novidades do bairro.',
|
|
]);
|
|
|
|
$this->actingAs($userA)
|
|
->postJson('/talk/person', [
|
|
'kind' => 'character',
|
|
'peer' => $them->id,
|
|
'message' => 'Oi, tudo bem?',
|
|
])
|
|
->assertCreated();
|
|
|
|
$peerEmail = $userB->email;
|
|
$peerName = $them->name;
|
|
Http::assertSent(function ($request) use ($peerEmail, $peerName): bool {
|
|
$payload = $request->data();
|
|
$encoded = json_encode($payload, JSON_UNESCAPED_UNICODE);
|
|
$system = (string) ($payload['messages'][0]['content'] ?? '');
|
|
|
|
return $payload['model'] === 'liquid/lfm-2.5-2.6b:free'
|
|
&& str_contains($system, 'Idade: 34')
|
|
&& str_contains($system, 'Sexo: feminino')
|
|
&& str_contains($system, 'Extrovertido e curiosa')
|
|
&& str_contains($system, 'Moradora do centro')
|
|
&& str_contains($system, 'Nome: '.$peerName)
|
|
&& ! str_contains($encoded, $peerEmail);
|
|
});
|
|
|
|
// The reply from the fake model is stored normally.
|
|
$this->assertNotNull(PersonTalk::query()
|
|
->where('from_character_id', $them->id)
|
|
->where('body', 'Oi! Tudo tranquilo na cidade.')
|
|
->first());
|
|
}
|
|
|
|
/**
|
|
* Fresh citizens are created with a ready-to-use default persona.
|
|
*/
|
|
public function test_new_characters_receive_default_persona(): void
|
|
{
|
|
[, $character] = $this->makeCitizen();
|
|
|
|
$this->assertNotNull($character->ai_age);
|
|
$this->assertGreaterThanOrEqual(18, $character->ai_age);
|
|
$this->assertLessThanOrEqual(68, $character->ai_age);
|
|
$this->assertContains($character->ai_personality, Character::AI_PERSONALITIES);
|
|
}
|
|
}
|