Add WikiLinkResolver with deterministic 5-step resolution

Prefers current locale, then document default_locale, then any locale
(lowest document_id), then slug match (legacy).
This commit is contained in:
Yutaka Kurosaki
2026-05-10 12:19:57 +09:00
parent 0c399c9f0f
commit d7522f592d
2 changed files with 152 additions and 0 deletions
@@ -0,0 +1,88 @@
<?php
namespace Tests\Unit\Services;
use App\Models\Document;
use App\Models\DocumentTranslation;
use App\Services\WikiLinkResolver;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class WikiLinkResolverTest extends TestCase
{
use RefreshDatabase;
private WikiLinkResolver $resolver;
protected function setUp(): void
{
parent::setUp();
$this->resolver = new WikiLinkResolver();
}
public function test_resolves_via_current_locale_title(): void
{
$doc = Document::factory()->create(['default_locale' => 'en', 'slug' => 'getting-started']);
$doc->translations()->where('locale', 'en')->update(['title' => 'Getting Started']);
DocumentTranslation::factory()->create([
'document_id' => $doc->id,
'locale' => 'ja',
'title' => 'はじめに',
]);
$resolved = $this->resolver->resolve('はじめに', 'ja');
$this->assertSame($doc->id, $resolved->id);
}
public function test_resolves_via_default_locale_when_current_locale_missing(): void
{
$doc = Document::factory()->create(['default_locale' => 'en']);
$doc->translations()->where('locale', 'en')->update(['title' => 'Getting Started']);
$resolved = $this->resolver->resolve('Getting Started', 'ja');
$this->assertSame($doc->id, $resolved->id);
}
public function test_resolves_via_any_locale_deterministically(): void
{
// Two documents both have a 'fr' translation titled "Bonjour", neither is current/default
$docA = Document::factory()->create(['default_locale' => 'en']);
DocumentTranslation::factory()->create(['document_id' => $docA->id, 'locale' => 'fr', 'title' => 'Bonjour']);
$docB = Document::factory()->create(['default_locale' => 'en']);
DocumentTranslation::factory()->create(['document_id' => $docB->id, 'locale' => 'fr', 'title' => 'Bonjour']);
$resolved = $this->resolver->resolve('Bonjour', 'ja');
// Lower id wins (deterministic)
$this->assertSame($docA->id, $resolved->id);
}
public function test_resolves_via_slug_when_no_title_match(): void
{
$doc = Document::factory()->create(['slug' => 'unique-slug', 'default_locale' => 'en']);
$doc->translations()->where('locale', 'en')->update(['title' => 'Whatever']);
$resolved = $this->resolver->resolve('unique-slug', 'ja');
$this->assertSame($doc->id, $resolved->id);
}
public function test_returns_null_when_nothing_matches(): void
{
$this->assertNull($this->resolver->resolve('Nonexistent', 'en'));
}
public function test_current_locale_wins_over_default(): void
{
// Doc A has en title "Setup"; Doc B has ja title "Setup"
$docA = Document::factory()->create(['default_locale' => 'en']);
$docA->translations()->where('locale', 'en')->update(['title' => 'Setup']);
$docB = Document::factory()->create(['default_locale' => 'en']);
$docB->translations()->where('locale', 'en')->update(['title' => 'Different']);
DocumentTranslation::factory()->create(['document_id' => $docB->id, 'locale' => 'ja', 'title' => 'Setup']);
// Browsing in ja: ja-locale match (Doc B) should win over default-locale match (Doc A)
$resolved = $this->resolver->resolve('Setup', 'ja');
$this->assertSame($docB->id, $resolved->id);
}
}