From a3ce9cad32320e87dd9322fdfb14b13af4785e48 Mon Sep 17 00:00:00 2001 From: sou7 & Hermes Date: Sun, 17 May 2026 23:13:06 +0900 Subject: [PATCH 01/12] =?UTF-8?q?=E3=83=86=E3=82=B9=E3=83=88=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0:=20YAML=E6=A7=8B=E6=96=87=E3=82=A8=E3=83=A9=E3=83=BC?= =?UTF-8?q?=E3=81=AE=E3=83=8F=E3=83=B3=E3=83=89=E3=83=AA=E3=83=B3=E3=82=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 無効なfrontmatterを持つノートでVault初期化のテスト - 無効なfrontmatterが存在しても他のノートが読み込まれるテスト - 無効なfrontmatterのノート自体も読み込まれるテスト - 無効なfrontmatterが存在してもlist機能が機能するテスト --- test/test_mcp.rb | 62 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/test/test_mcp.rb b/test/test_mcp.rb index 6a84388..e5efb36 100644 --- a/test/test_mcp.rb +++ b/test/test_mcp.rb @@ -167,3 +167,65 @@ def test_backlink assert content.include?('Hello World'), "バックリンクが見つかりませんでした" end end + +# YAML構文エラーのハンドリングをテスト +class VaultYamlErrorTest < Minitest::Test + def setup + @test_vault = Dir.mktmpdir('obsidian_yaml_test_') + + # FIXTURE_VAULT から既存のファイルをコピー + FileUtils.cp_r(FIXTURE_VAULT, @test_vault) + + # 無効なfrontmatterを持つ.mdファイルを追加 + # YAML構文エラー: エスケープされていない引用符 + invalid_frontmatter_content = <<~MD + --- + title: "未完了の文字列 + aliases: + - broken + --- + # Invalid YAML Note + MD + File.write(File.join(@test_vault, 'Invalid Frontmatter.md'), invalid_frontmatter_content) + end + + def teardown + FileUtils.rm_rf(@test_vault) if @test_vault + end + + def test_vault_initializes_with_invalid_yaml_frontmatter + # 無効なfrontmatterが存在しても、Vault は初期化できることを確認 + # Psych::SyntaxError が発生しても collect_notes は次のファイルに進む + vault = ObsidianFetch::Vault.new([@test_vault]) + refute_nil vault, 'Vault が初期化されませんでした' + end + + def test_valid_notes_loaded_despite_invalid_frontmatter + # 無効なfrontmatterが存在しても、他のノートが読み込まれることを確認 + vault = ObsidianFetch::Vault.new([@test_vault]) + + # Hello World ノートが読み込まれていることを確認 + result = vault.tool_read('Hello World') + refute result.error, "Hello World ノートの読み込みに失敗しました: #{result.text}" + assert result.text.include?('Hello World'), "Hello World ノートの内容が見つかりませんでした" + end + + def test_invalid_yaml_note_is_still_loaded + # 無効なfrontmatterのノートも読み込まれていることを確認 + # (frontmatter解析はスキップされるが、ファイル自体は読み込まれる) + vault = ObsidianFetch::Vault.new([@test_vault]) + + result = vault.tool_read('Invalid Frontmatter') + refute result.error, "Invalid Frontmatter ノートの読み込みに失敗しました: #{result.text}" + assert result.text.include?('Invalid YAML Note'), "Invalid Frontmatter ノートの内容が見つかりませんでした" + end + + def test_list_works_with_invalid_frontmatter + # 無効なfrontmatterが存在しても、list が機能することを確認 + vault = ObsidianFetch::Vault.new([@test_vault]) + + result = vault.tool_list('Hello') + refute result.error, "リストの取得に失敗しました: #{result.text}" + assert result.text.include?('Hello'), "Hello で始まるノートが見つかりませんでした" + end +end From 14d71d98ce71221db6c7dfe065797fd340db4f82 Mon Sep 17 00:00:00 2001 From: sou7 & Hermes Date: Sun, 17 May 2026 23:15:08 +0900 Subject: [PATCH 02/12] =?UTF-8?q?=E3=83=86=E3=82=B9=E3=83=88=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0:=20tool=5Fread=20=E3=81=AE=E3=83=91=E3=82=B9=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3=E7=B5=8C=E8=B7=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - パスを含む名前でtool_readを呼び出した場合のテスト - basenameで再試行されノートが読み込まれるテスト - prefaceメッセージが含まれるテスト --- test/test_mcp.rb | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/test/test_mcp.rb b/test/test_mcp.rb index e5efb36..b536be1 100644 --- a/test/test_mcp.rb +++ b/test/test_mcp.rb @@ -229,3 +229,32 @@ def test_list_works_with_invalid_frontmatter assert result.text.include?('Hello'), "Hello で始まるノートが見つかりませんでした" end end + +# tool_read のパス修正経路をテスト +class VaultPathCorrectionTest < Minitest::Test + def setup + @test_vault = Dir.mktmpdir('obsidian_path_test_') + FileUtils.cp_r(FIXTURE_VAULT, @test_vault) + end + + def teardown + FileUtils.rm_rf(@test_vault) if @test_vault + end + + def test_tool_read_with_slash_in_name_falls_back_to_basename + # パスを含む名前で読み取ると、basename で再試行され、ノートが読み込まれる + vault = ObsidianFetch::Vault.new([@test_vault]) + + # "some/path/Hello World" のようにパスを含む名前で読み取る + result = vault.tool_read('some/path/Hello World') + + # エラーにならない + refute result.error, "パスを含む名前の読み込みに失敗しました: #{result.text}" + + # "Hello World" ノートの内容が含まれる + assert result.text.include?('Hello World'), "Hello World ノートの内容が見つかりませんでした" + + # prefaceメッセージが含まれる(パス修正のログ) + assert result.text.include?('renamed'), "prefaceメッセージが見つかりませんでした: #{result.text}" + end +end From 2bc36ee0c0908e71b1e1bcb164c59a4b42615a90 Mon Sep 17 00:00:00 2001 From: sou7 & Hermes Date: Sun, 17 May 2026 23:17:15 +0900 Subject: [PATCH 03/12] =?UTF-8?q?=E3=83=86=E3=82=B9=E3=83=88=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0:=20tool=5Flist=20=E3=81=AE=E7=A9=BA=E6=96=87=E5=AD=97?= =?UTF-8?q?=E5=88=97=E6=A4=9C=E7=B4=A2=20+=20=E3=82=B3=E3=83=BC=E3=83=89?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 空文字でtool_listを呼び出した場合のテスト - Result.new の引数修正 (text: キーワード引数追加) --- lib/obsidian_fetch.rb | 6 +++--- test/test_mcp.rb | 9 +++++++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/obsidian_fetch.rb b/lib/obsidian_fetch.rb index 579159a..add390f 100644 --- a/lib/obsidian_fetch.rb +++ b/lib/obsidian_fetch.rb @@ -220,9 +220,9 @@ def tool_list name # 空白文字で検索された場合は失敗した旨を返す # 仮に全ノートからランダムなMAX_LIST_SIZEを返してしまうと、LLMが誤って呼び出した場合に偽の関連性を持ってしまうため if split_name.empty? - return Result.new(<<~EOS, error: true) - It cannot be listed in a blank string. - EOS + return Result.new(text: <<~EOS, error: true) + It cannot be listed in a blank string. + EOS end matched_notes = @notes.select do |note_name, _file_pathes| split_name.map {|name_part| note_name.include?(name_part) }.all? diff --git a/test/test_mcp.rb b/test/test_mcp.rb index b536be1..90f2548 100644 --- a/test/test_mcp.rb +++ b/test/test_mcp.rb @@ -228,6 +228,15 @@ def test_list_works_with_invalid_frontmatter refute result.error, "リストの取得に失敗しました: #{result.text}" assert result.text.include?('Hello'), "Hello で始まるノートが見つかりませんでした" end + + def test_tool_list_with_blank_string_returns_error + # 空文字列で tool_list を呼び出した場合、エラーが返されることを確認 + vault = ObsidianFetch::Vault.new([@test_vault]) + + result = vault.tool_list('') + assert result.error, "空文字列で検索してもエラーが返されませんでした: #{result.text}" + assert result.text.include?('blank') || result.text.include?('cannot be listed'), "エラーメッセージに期待する文言が含まれていません: #{result.text}" + end end # tool_read のパス修正経路をテスト From 39d54daf41df6a3354b3cb31df860a4fed7b7c1e Mon Sep 17 00:00:00 2001 From: sou7 & Hermes Date: Sun, 17 May 2026 23:18:57 +0900 Subject: [PATCH 04/12] =?UTF-8?q?=E3=83=86=E3=82=B9=E3=83=88=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0:=20tool=5Flist=20=E3=81=AE=E5=A4=A7=E9=87=8F=E3=83=9E?= =?UTF-8?q?=E3=83=83=E3=83=81=E5=88=B6=E9=99=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 25個のノートを追加し、20個以下のノートが返されるテスト - MAX_LIST_SIZE の制限が適用されるテスト --- test/test_mcp.rb | 68 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/test/test_mcp.rb b/test/test_mcp.rb index 90f2548..e99aa09 100644 --- a/test/test_mcp.rb +++ b/test/test_mcp.rb @@ -267,3 +267,71 @@ def test_tool_read_with_slash_in_name_falls_back_to_basename assert result.text.include?('renamed'), "prefaceメッセージが見つかりませんでした: #{result.text}" end end + +# tool_list の大量マッチ制限 (MAX_LIST_SIZE) をテスト +class VaultMassLimitTest < Minitest::Test + def setup + @test_vault = Dir.mktmpdir('obsidian_mass_test_') + + # FIXTURE_VAULT から既存のファイルをコピー + FileUtils.cp_r(FIXTURE_VAULT, @test_vault) + + # 25個のノートを作成(MAX_LIST_SIZE=20を超える) + 25.times do |i| + note_name = "MassNote_#{sprintf('%02d', i)}" + File.write(File.join(@test_vault, "#{note_name}.md"), "# #{note_name}\n") + end + end + + def teardown + FileUtils.rm_rf(@test_vault) if @test_vault + end + + def test_tool_list_limits_results_to_max_list_size + # 25個のノートが存在することを確認 + notes_in_vault = Dir.glob(File.join(@test_vault, '**', '*.md')).count + assert notes_in_vault >= 25, "Vault に 25個以上のノートがありません: #{notes_in_vault}" + + vault = ObsidianFetch::Vault.new([@test_vault]) + + # tool_list を呼び出す(MassNote_ で始まるノートを検索) + result = vault.tool_list('MassNote_') + + # エラーにならない + refute result.error, "tool_list の呼び出しに失敗しました: #{result.text}" + + # 返されたノート名をカウント + note_names = result.text.scan(/MassNote_\d+/).uniq + assert note_names.length <= 20, "MAX_LIST_SIZE(20)を超えるノートが返されました: #{note_names.length}" + end + + def test_tool_list_mass_limit_has_warning_message + vault = ObsidianFetch::Vault.new([@test_vault]) + + result = vault.tool_list('MassNote_') + + # 大量マッチ制限が適用された場合は警告メッセージが含まれる + # 25個のノートが存在し、20個以下しか返されない + note_names = result.text.scan(/MassNote_\d+/).uniq + if note_names.length < 25 + # 制限が適用された場合、何かしらの制限に関するメッセージが期待される + # ただしエラーではなく、警告として処理される + refute result.error, "大量マッチ制限がエラーとして返されました: #{result.text}" + end + end + + def test_tool_list_returns_at_most_20_notes + vault = ObsidianFetch::Vault.new([@test_vault]) + + result = vault.tool_list('MassNote_') + + refute result.error, "tool_list の呼び出しに失敗しました: #{result.text}" + + # 返されたノート名の数が 20以下であることを確認 + note_names = result.text.scan(/MassNote_\d+/).uniq + assert note_names.length <= 20, "20個を超えるノートが返されました: #{note_names.length}" + + # 25個のノートを作成したので、20個以下しか返されない + assert note_names.length < 25, "25個全てのノートが返されました(制限が機能していません)" + end +end From 2b4965b71f7741eb77a007edc8a43db310cef539 Mon Sep 17 00:00:00 2001 From: sou7 & Hermes Date: Sun, 17 May 2026 23:21:12 +0900 Subject: [PATCH 05/12] =?UTF-8?q?=E3=83=86=E3=82=B9=E3=83=88=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0:=20tool=5Flist=20=E3=81=AE=E3=83=90=E3=83=83=E3=82=AF?= =?UTF-8?q?=E3=83=AA=E3=83=B3=E3=82=AF=E3=83=95=E3=82=A9=E3=83=BC=E3=83=AB?= =?UTF-8?q?=E3=83=90=E3=83=83=E3=82=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ノート名がマッチせずとも @links_by_file_name に存在する場合のテスト - バックリンクが表示されるテスト --- test/test_mcp.rb | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/test/test_mcp.rb b/test/test_mcp.rb index e99aa09..4cc20f3 100644 --- a/test/test_mcp.rb +++ b/test/test_mcp.rb @@ -335,3 +335,42 @@ def test_tool_list_returns_at_most_20_notes assert note_names.length < 25, "25個全てのノートが返されました(制限が機能していません)" end end + +# tool_list のバックリンクフォールバックをテスト +class VaultBacklinkFallbackTest < Minitest::Test + def setup + @test_vault = Dir.mktmpdir('obsidian_backlink_test_') + FileUtils.cp_r(FIXTURE_VAULT, @test_vault) + + # リンク先が存在しないノートを追加 + File.write( + File.join(@test_vault, 'Links to NonExistent.md'), + "# Links to NonExistent\n\nThis note links to [[Phantom Note]].\n" + ) + end + + def teardown + FileUtils.rm_rf(@test_vault) if @test_vault + end + + def test_tool_list_backlink_fallback + vault = ObsidianFetch::Vault.new([@test_vault]) + + # "Phantom Note" はノート名として存在しない(@notes に登録されない) + matched = vault.notes.key?('Phantom Note') + refute matched, "Phantom Note がノート名として登録されていました" + + # だが @links_by_file_name には存在する(他のノートからリンクされている) + assert vault.links_by_file_name.key?('Phantom Note'), "links_by_file_name に Phantom Note が登録されていません" + + # tool_list を呼び出す + result = vault.tool_list('Phantom Note') + + # エラーは true である(ノートが見つからないため) + assert result.error, "エラーが返されませんでした: #{result.text}" + + # バックリンクが表示されることを確認 + assert result.text.include?('However, I found other notes linked to this note'), "バックリンクのメッセージが表示されませんでした: #{result.text}" + assert result.text.include?('Links to NonExistent'), "バックリンク先ノート名が表示されませんでした: #{result.text}" + end +end From e89ebd698b41210b1d869d9506fddf0c7498c8b5 Mon Sep 17 00:00:00 2001 From: sou7 & Hermes Date: Sun, 17 May 2026 23:23:11 +0900 Subject: [PATCH 06/12] =?UTF-8?q?=E3=83=86=E3=82=B9=E3=83=88=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0:=20[[link|displayname]]=20=E5=BD=A2=E5=BC=8F=E3=81=AE?= =?UTF-8?q?=E3=83=AA=E3=83=B3=E3=82=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - [[link|displayname]] 形式のリンクが正しく収集されるテスト - displayname がリンク先として使用されないテスト --- test/test_mcp.rb | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/test/test_mcp.rb b/test/test_mcp.rb index 4cc20f3..cc049ba 100644 --- a/test/test_mcp.rb +++ b/test/test_mcp.rb @@ -336,6 +336,46 @@ def test_tool_list_returns_at_most_20_notes end end +# [[link|displayname]] 形式のリンクをテスト +class VaultDisplayNameLinkTest < Minitest::Test + def setup + @test_vault = Dir.mktmpdir('obsidian_displayname_test_') + FileUtils.cp_r(FIXTURE_VAULT, @test_vault) + + # [[link|displayname]] 形式のリンクを含むノートを追加 + File.write( + File.join(@test_vault, 'Has DisplayName Link.md'), + "# Has DisplayName Link\n\nThis note has a [[Hello World|HW Note]] link.\n" + ) + end + + def teardown + FileUtils.rm_rf(@test_vault) if @test_vault + end + + def test_displayname_link_is_collected + vault = ObsidianFetch::Vault.new([@test_vault]) + + # [[link|displayname]] の link 部分 (Hello World) が links_by_file_name に登録される + assert vault.links_by_file_name.key?('Hello World'), "links_by_file_name に 'Hello World' が登録されていません" + end + + def test_displayname_link_is_correct_target + vault = ObsidianFetch::Vault.new([@test_vault]) + + # links_by_file_name['Hello World'] のバックリンクに 'Has DisplayName Link' が含まれている + backlinks = vault.links_by_file_name['Hello World'] + assert backlinks.any? { |p| p.include?('Has DisplayName Link') }, "links_by_file_name['Hello World'] に 'Has DisplayName Link' が含まれていません: #{backlinks}" + end + + def test_displayname_link_does_not_use_displayname_as_target + vault = ObsidianFetch::Vault.new([@test_vault]) + + # displayname (HW Note) がリンク先として登録されない + refute vault.links_by_file_name.key?('HW Note'), "displayname 'HW Note' がリンク先として登録されてはいけません" + end +end + # tool_list のバックリンクフォールバックをテスト class VaultBacklinkFallbackTest < Minitest::Test def setup From a3e0f60f8c1bed0e2aa3c93f2a51a7167622bc9a Mon Sep 17 00:00:00 2001 From: sou7 & Hermes Date: Sun, 17 May 2026 23:25:41 +0900 Subject: [PATCH 07/12] =?UTF-8?q?=E3=83=86=E3=82=B9=E3=83=88=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0:=20[display](path)=20=E5=BD=A2=E5=BC=8F=E3=81=AE?= =?UTF-8?q?=E3=83=AA=E3=83=B3=E3=82=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - [display](path) 形式のリンクが正しく収集されるテスト - .md 拡張子付きリンクのテスト - 外部URLが除外されるテスト --- test/test_mcp.rb | 67 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/test/test_mcp.rb b/test/test_mcp.rb index cc049ba..af43fee 100644 --- a/test/test_mcp.rb +++ b/test/test_mcp.rb @@ -376,6 +376,73 @@ def test_displayname_link_does_not_use_displayname_as_target end end +# `[display](path)` 形式のリンクをテスト +class VaultDisplayPathLinkTest < Minitest::Test + def setup + @test_vault = Dir.mktmpdir('obsidian_displaypath_test_') + FileUtils.cp_r(FIXTURE_VAULT, @test_vault) + + # `[display](path)` 形式のリンクを含むノートを追加 + File.write( + File.join(@test_vault, 'Has Display Path Link.md'), + "# Has Display Path Link\n\nThis note has a [Link to HW](Hello World) link.\n" + ) + end + + def teardown + FileUtils.rm_rf(@test_vault) if @test_vault + end + + def test_displaypath_link_is_collected + vault = ObsidianFetch::Vault.new([@test_vault]) + + # `[display](path)` の path 部分 (Hello World) が links_by_file_name に登録される + assert vault.links_by_file_name.key?('Hello World'), "links_by_file_name に 'Hello World' が登録されていません" + end + + def test_displaypath_link_backlink_source + vault = ObsidianFetch::Vault.new([@test_vault]) + + # links_by_file_name['Hello World'] のバックリンクに 'Has Display Path Link' が含まれている + backlinks = vault.links_by_file_name['Hello World'] + assert backlinks.any? { |p| p.include?('Has Display Path Link') }, "links_by_file_name['Hello World'] に 'Has Display Path Link' が含まれていません: #{backlinks}" + end + + def test_displaypath_link_does_not_use_display_as_target + vault = ObsidianFetch::Vault.new([@test_vault]) + + # display 部分 (Link to HW) がリンク先として登録されない + refute vault.links_by_file_name.key?('Link to HW'), "display 'Link to HW' がリンク先として登録されてはいけません" + end + + def test_displaypath_link_with_md_extension + # `[display](path.md)` 形式のリンクもテスト + File.write( + File.join(@test_vault, 'Has Display Path Link with MD.md'), + "# Has Display Path Link with MD\n\nThis note has a [Link to HW](Hello World.md) link.\n" + ) + + vault = ObsidianFetch::Vault.new([@test_vault]) + + # .md を除去して 'Hello World' がリンク先として登録される + assert vault.links_by_file_name.key?('Hello World'), "links_by_file_name に 'Hello World' が登録されていません" + end + + def test_displaypath_link_external_url_excluded + # 外部リンクは除外されることをテスト + File.write( + File.join(@test_vault, 'Has External Link.md'), + "# Has External Link\n\nThis note has a [External Link](https://example.com) link.\n" + ) + + vault = ObsidianFetch::Vault.new([@test_vault]) + + # 外部リンクのパスが links_by_file_name に登録されない + refute vault.links_by_file_name.key?('example.com'), "外部リンク 'example.com' がリンク先として登録されてはいけません" + refute vault.links_by_file_name.key?('https'), "外部リンク 'https' がリンク先として登録されてはいけません" + end +end + # tool_list のバックリンクフォールバックをテスト class VaultBacklinkFallbackTest < Minitest::Test def setup From be1eedca5c904e9411880685044efc9e990ee058 Mon Sep 17 00:00:00 2001 From: sou7 & Hermes Date: Sun, 17 May 2026 23:27:22 +0900 Subject: [PATCH 08/12] =?UTF-8?q?=E3=83=86=E3=82=B9=E3=83=88=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0:=20.md=20=E6=8B=A1=E5=BC=B5=E5=AD=90=E4=BB=98?= =?UTF-8?q?=E3=81=8D=E3=83=AA=E3=83=B3=E3=82=AF=E3=81=AE=E5=87=A6=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - [[Note.md]] 形式のリンクが .md を除去して正しく収集されるテスト - .md 拡張子付きリンクのバックリンクのテスト --- test/test_mcp.rb | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/test/test_mcp.rb b/test/test_mcp.rb index af43fee..05cbd7f 100644 --- a/test/test_mcp.rb +++ b/test/test_mcp.rb @@ -336,6 +336,46 @@ def test_tool_list_returns_at_most_20_notes end end +# `[[Note.md]]` 形式の `.md` 拡張子付きリンクをテスト +class VaultMdExtensionLinkTest < Minitest::Test + def setup + @test_vault = Dir.mktmpdir('obsidian_md_extension_test_') + FileUtils.cp_r(FIXTURE_VAULT, @test_vault) + + # [[Note.md]] 形式のリンクを含むノートを追加 + File.write( + File.join(@test_vault, 'Has MD Link.md'), + "# Has MD Link\n\nThis note has a [[Hello World.md]] link.\n" + ) + end + + def teardown + FileUtils.rm_rf(@test_vault) if @test_vault + end + + def test_md_extension_link_is_stripped + vault = ObsidianFetch::Vault.new([@test_vault]) + + # [[Hello World.md]] の .md が除去され、'Hello World' が links_by_file_name に登録される + assert vault.links_by_file_name.key?('Hello World'), "links_by_file_name に 'Hello World' が登録されていません" + end + + def test_md_extension_link_backlink_correct + vault = ObsidianFetch::Vault.new([@test_vault]) + + # links_by_file_name['Hello World'] のバックリンクに 'Has MD Link' が含まれている + backlinks = vault.links_by_file_name['Hello World'] + assert backlinks.any? { |p| p.include?('Has MD Link') }, "links_by_file_name['Hello World'] に 'Has MD Link' が含まれていません: #{backlinks}" + end + + def test_md_extension_link_does_not_register_with_extension + vault = ObsidianFetch::Vault.new([@test_vault]) + + # .md 拡張子付きで登録されない + refute vault.links_by_file_name.key?('Hello World.md'), "links_by_file_name に 'Hello World.md' が登録されてはいけません" + end +end + # [[link|displayname]] 形式のリンクをテスト class VaultDisplayNameLinkTest < Minitest::Test def setup From 755e8c670ad2c0e9920fcb429ae0cdf5012a3b20 Mon Sep 17 00:00:00 2001 From: sou7 & Hermes Date: Sun, 17 May 2026 23:29:51 +0900 Subject: [PATCH 09/12] =?UTF-8?q?=E3=83=86=E3=82=B9=E3=83=88=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0:=20#=20=E3=82=A2=E3=83=B3=E3=82=AB=E3=83=BC=E4=BB=98?= =?UTF-8?q?=E3=81=8D=E3=83=AA=E3=83=B3=E3=82=AF=E3=81=AE=E5=87=A6=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - [[Note#section]] 形式のリンクが # を除去して正しく収集されるテスト - ^block_id のテスト --- test/test_mcp.rb | 53 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/test/test_mcp.rb b/test/test_mcp.rb index 05cbd7f..063acd4 100644 --- a/test/test_mcp.rb +++ b/test/test_mcp.rb @@ -521,3 +521,56 @@ def test_tool_list_backlink_fallback assert result.text.include?('Links to NonExistent'), "バックリンク先ノート名が表示されませんでした: #{result.text}" end end + +# `[[Note#section]]` 形式のリンクをテスト(#アンカー処理) +class VaultAnchorLinkTest < Minitest::Test + def setup + @test_vault = Dir.mktmpdir('obsidian_anchor_test_') + FileUtils.cp_r(FIXTURE_VAULT, @test_vault) + + # [[Note#section]] 形式のリンクを含むノートを追加 + File.write( + File.join(@test_vault, 'Has Anchor Link.md'), + "# Has Anchor Link\n\nThis note has a [[Hello World#Introduction]] link.\n" + ) + end + + def teardown + FileUtils.rm_rf(@test_vault) if @test_vault + end + + def test_anchor_link_is_stripped + vault = ObsidianFetch::Vault.new([@test_vault]) + + # [[Hello World#Introduction]] の #アンカーが除去され、'Hello World' が links_by_file_name に登録される + assert vault.links_by_file_name.key?('Hello World'), "links_by_file_name に 'Hello World' が登録されていません" + end + + def test_anchor_link_backlink_correct + vault = ObsidianFetch::Vault.new([@test_vault]) + + # links_by_file_name['Hello World'] のバックリンクに 'Has Anchor Link' が含まれている + backlinks = vault.links_by_file_name['Hello World'] + assert backlinks.any? { |p| p.include?('Has Anchor Link') }, "links_by_file_name['Hello World'] に 'Has Anchor Link' が含まれていません: #{backlinks}" + end + + def test_anchor_link_does_not_register_with_anchor + vault = ObsidianFetch::Vault.new([@test_vault]) + + # アンカー付きのまま登録されない + refute vault.links_by_file_name.key?('Hello World#Introduction'), "links_by_file_name に 'Hello World#Introduction' が登録されてはいけません" + end + + def test_anchor_link_with_block_id + # ^block_id 形式のリンクもテスト + File.write( + File.join(@test_vault, 'Has Block Link.md'), + "# Has Block Link\n\nThis note has a [[Hello World^blockid]] link.\n" + ) + + vault = ObsidianFetch::Vault.new([@test_vault]) + + # ^blockid が除去され、'Hello World' が links_by_file_name に登録される + assert vault.links_by_file_name.key?('Hello World'), "links_by_file_name に 'Hello World' が登録されていません" + end +end From 0fa652666fc1e38a3af3aebd2c26c20792f3b5a3 Mon Sep 17 00:00:00 2001 From: sou7 & Hermes Date: Sun, 17 May 2026 23:32:25 +0900 Subject: [PATCH 10/12] =?UTF-8?q?=E3=83=86=E3=82=B9=E3=83=88=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0:=20tool=5Fread=20=E3=81=AE=E3=83=90=E3=83=83=E3=82=AF?= =?UTF-8?q?=E3=83=AA=E3=83=B3=E3=82=AF=E8=A1=A8=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - @links_by_file_name に存在する場合のテスト - バックリンクが表示されるテスト --- test/test_mcp.rb | 55 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/test/test_mcp.rb b/test/test_mcp.rb index 063acd4..1de4550 100644 --- a/test/test_mcp.rb +++ b/test/test_mcp.rb @@ -522,6 +522,61 @@ def test_tool_list_backlink_fallback end end +# `tool_read` のバックリンク表示をテスト +class VaultToolReadBacklinkTest < Minitest::Test + def setup + @test_vault = Dir.mktmpdir('obsidian_backlink_display_test_') + FileUtils.cp_r(FIXTURE_VAULT, @test_vault) + + # リンク元となるノートを追加(Hello World を参照する) + File.write( + File.join(@test_vault, 'Refers to Hello.md'), + "# Refers to Hello\n\nThis note has a [[Hello World]] link.\n" + ) + end + + def teardown + FileUtils.rm_rf(@test_vault) if @test_vault + end + + def test_tool_read_shows_backlink_when_links_by_file_name_exists + vault = ObsidianFetch::Vault.new([@test_vault]) + + # 'Refers to Hello' が Hello World を参照しているので、 + # @links_by_file_name['Hello World'] に 'Refers to Hello' が登録される + assert vault.links_by_file_name.key?('Hello World'), "links_by_file_name に 'Hello World' が登録されていません" + + # Hello World ノートを tool_read で読み取る + result = vault.tool_read('Hello World') + + # ノートが読み込まれていることを確認 + refute result.error, "Hello World ノートの読み込みに失敗しました: #{result.text}" + assert result.text.include?('Hello World'), "Hello World ノートの内容が見つかりませんでした" + + # バックリンクが表示されることを確認 + assert result.text.include?('Refers to Hello'), "バックリンク 'Refers to Hello' が表示されませんでした: #{result.text}" + end + + def test_tool_read_backlink_shows_linked_by_message + vault = ObsidianFetch::Vault.new([@test_vault]) + + result = vault.tool_read('Hello World') + + # バックリンクのセクションヘッダーが表示されていることを確認 + assert result.text.include?('linked by') || result.text.include?('This note is linked'), "バックリンクのメッセージが表示されませんでした: #{result.text}" + end + + def test_tool_read_backlink_lists_all_linking_notes + vault = ObsidianFetch::Vault.new([@test_vault]) + + result = vault.tool_read('Hello World') + + # バックリンクに複数のリンク元ノートが表示されていることを確認 + assert result.text.include?('Refers to Hello'), "バックリンクに 'Refers to Hello' が表示されませんでした: #{result.text}" + assert result.text.include?('Links to HW'), "バックリンクに 'Links to HW' が表示されませんでした: #{result.text}" + end +end + # `[[Note#section]]` 形式のリンクをテスト(#アンカー処理) class VaultAnchorLinkTest < Minitest::Test def setup From 49032a225fa75280b990945a02435194fda7d0e4 Mon Sep 17 00:00:00 2001 From: sou7 & Hermes Date: Mon, 18 May 2026 01:09:07 +0900 Subject: [PATCH 11/12] =?UTF-8?q?=E4=BF=AE=E6=AD=A3:=20Result.new=20?= =?UTF-8?q?=E3=81=AE=E5=BC=95=E6=95=B0=E4=BF=AE=E6=AD=A3=E3=82=92=E6=88=BB?= =?UTF-8?q?=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/obsidian_fetch.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/obsidian_fetch.rb b/lib/obsidian_fetch.rb index add390f..579159a 100644 --- a/lib/obsidian_fetch.rb +++ b/lib/obsidian_fetch.rb @@ -220,9 +220,9 @@ def tool_list name # 空白文字で検索された場合は失敗した旨を返す # 仮に全ノートからランダムなMAX_LIST_SIZEを返してしまうと、LLMが誤って呼び出した場合に偽の関連性を持ってしまうため if split_name.empty? - return Result.new(text: <<~EOS, error: true) - It cannot be listed in a blank string. - EOS + return Result.new(<<~EOS, error: true) + It cannot be listed in a blank string. + EOS end matched_notes = @notes.select do |note_name, _file_pathes| split_name.map {|name_part| note_name.include?(name_part) }.all? From 06d7fc7bc1bc2bbe6b00576873887e2ac8bb8aa7 Mon Sep 17 00:00:00 2001 From: sou7 & Hermes Date: Mon, 18 May 2026 01:13:13 +0900 Subject: [PATCH 12/12] =?UTF-8?q?=E4=BF=AE=E6=AD=A3:=20Result.new=20?= =?UTF-8?q?=E3=81=AE=20text:=20=E3=82=AD=E3=83=BC=E3=83=AF=E3=83=BC?= =?UTF-8?q?=E3=83=89=E5=BC=95=E6=95=B0=E3=82=92=E8=BF=BD=E5=8A=A0=E3=81=97?= =?UTF-8?q?=E3=81=A6=E3=82=A4=E3=83=B3=E3=83=87=E3=83=B3=E3=83=88=E3=82=92?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/obsidian_fetch.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/obsidian_fetch.rb b/lib/obsidian_fetch.rb index 579159a..a3ecc1b 100644 --- a/lib/obsidian_fetch.rb +++ b/lib/obsidian_fetch.rb @@ -220,7 +220,7 @@ def tool_list name # 空白文字で検索された場合は失敗した旨を返す # 仮に全ノートからランダムなMAX_LIST_SIZEを返してしまうと、LLMが誤って呼び出した場合に偽の関連性を持ってしまうため if split_name.empty? - return Result.new(<<~EOS, error: true) + return Result.new(text: <<~EOS, error: true) It cannot be listed in a blank string. EOS end