diff --git a/src/app/Services/WikiLinkResolver.php b/src/app/Services/WikiLinkResolver.php new file mode 100644 index 0000000..d25fb87 --- /dev/null +++ b/src/app/Services/WikiLinkResolver.php @@ -0,0 +1,64 @@ +where('title', $linkText) + ->orderBy('document_id') + ->first(); + if ($byCurrent) { + return $byCurrent->document; + } + + // 2. Document's default-locale title match + $byDefault = DocumentTranslation::query() + ->join('documents', 'documents.id', '=', 'document_translations.document_id') + ->whereColumn('document_translations.locale', 'documents.default_locale') + ->where('document_translations.title', $linkText) + ->orderBy('document_translations.document_id') + ->select('document_translations.*') + ->first(); + if ($byDefault) { + return $byDefault->document; + } + + // 3. Any-locale title match (lowest document_id wins) + $byAny = DocumentTranslation::where('title', $linkText) + ->orderBy('document_id') + ->first(); + if ($byAny) { + return $byAny->document; + } + + // 4. Slug match (legacy) + $slug = SlugHelper::generate($linkText); + $bySlug = Document::where('slug', $slug)->first(); + if ($bySlug) { + return $bySlug; + } + + // 5. Nothing + return null; + } +} diff --git a/src/tests/Unit/Services/WikiLinkResolverTest.php b/src/tests/Unit/Services/WikiLinkResolverTest.php new file mode 100644 index 0000000..ceb0fdd --- /dev/null +++ b/src/tests/Unit/Services/WikiLinkResolverTest.php @@ -0,0 +1,88 @@ +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); + } +}