diff options
| author | tanaka takashi <isazaka@gmail.com> | 2026-08-10 19:38:55 +0900 |
|---|---|---|
| committer | tanaka takashi <isazaka@gmail.com> | 2026-08-10 19:38:55 +0900 |
| commit | e8b625e22f343d75e1b50d1634b4ab06899d060b (patch) | |
| tree | 80b8d5d045f9402884a75cf2c74d1e15cb3ba9aa /tests/Agent.Core.Tests | |
| parent | f2001b0c5a0b35e052ba62ef4f2e19bcc64fd870 (diff) | |
feat(tools): add dir internal tool for directory operations
新規Internal Tool `dir` を追加し、ディレクトリの作成・削除・一覧取得を
可能にした。file_editの拡張ではなく独立toolとして実装し、LLMが
「ディレクトリ操作はdir tool」と直感的に判断できるようにした。
追加した機能:
- dir tool (6番目のInternal Tool、MCP非公開)
- mkdir: Write capability要求、Directory.CreateDirectory()で親ディレクトリも含めて作成
- rmdir: Delete capability要求、空ディレクトリのみ削除(非空はToolValidationExceptionで拒否)
- list: Read capability要求、直下エントリ一覧を返す
設計判断:
- file_edit拡張ではなく新規toolとした理由: ツール名から機能が推測しやすく、
LLMが直感的に選択できるため
- rmdirを空ディレクトリのみに制限した理由: rollback時の内容復元の複雑さを排除、
MemoryBackupLimit超過リスクなし、安全側
- mkdir/rmdirは既存のPathPolicy/ACL/Journal/Recovery枠組みを利用
- root継承ACE方式のため新規作成ディレクトリは自動的に親のACEを継承、追加ACL操作不要
変更対象:
- InternalToolCatalog.cs: dir定義追加 (5→6 tool)
- InternalToolDispatcher.cs: FakeInternalToolBackendの効果判定にdir追加
- InternalToolValidator.cs: dir用コメント追記
- WindowsToolBackend.cs: DirectoryOp()メソッド実装
- TransactionalFileEditor.cs: Mkdir()/Rmdir() transaction +
DirectoryMkdirTransaction/DirectoryRmdirTransaction クラス追加
- RecoveryManager.cs: dir.mkdir/dir.rmdir crash recovery追加
- Specifications.md: FS-06追加
- Design.md: 6群表更新、§7.6 dir追加、5群→6群参照更新
テスト:
- InternalToolTests.cs: dir単体テスト、catalog 6群検証、malformed args拒否
- SecurityPolicyTests.cs: mkdir/rmdir/list/許可外deny テスト追加
- PromptAndRuntimeTests.cs: tool count 5→6
- OllamaClientTests.cs: tool catalog配列長 5→6
検証結果:
- Release build: 警告0、エラー0
- Planner tests: 10/10 PASS、skip 0
- Core tests: 56/56 PASS、skip 0
- MCP経由での動作確認: mkdir/list/rmdir/file_edit すべて正常
- Python実行確認: dir mkdir + file_edit + process_run でfib(1)〜fib(10)が正常出力
- Release Bundle publish済み
既知の運用上の知見:
- uv venvのpyvenv.cfgのhomeがjunctionを指す場合、CanonicalPath.Resolveが
reparse pointとして拒否する。実体パスへ書き換えるか、実体ディレクトリを
agent.yamlのread/executeに追加する必要がある
- venvのpython.exeはuv trampolineで子プロセスを生成するため、
sandboxのchild-process制限で拒否される。pyvenv.cfgが指す実体python.exeを
直接起動する必要がある
Diffstat (limited to 'tests/Agent.Core.Tests')
| -rw-r--r-- | tests/Agent.Core.Tests/InternalToolTests.cs | 40 | ||||
| -rw-r--r-- | tests/Agent.Core.Tests/OllamaClientTests.cs | 2 | ||||
| -rw-r--r-- | tests/Agent.Core.Tests/PromptAndRuntimeTests.cs | 2 |
3 files changed, 40 insertions, 4 deletions
diff --git a/tests/Agent.Core.Tests/InternalToolTests.cs b/tests/Agent.Core.Tests/InternalToolTests.cs index e452cab..2015702 100644 --- a/tests/Agent.Core.Tests/InternalToolTests.cs +++ b/tests/Agent.Core.Tests/InternalToolTests.cs @@ -6,9 +6,9 @@ namespace Agent.Core.Tests; public sealed class InternalToolTests { [Fact] - public void Catalog_exposes_exactly_five_strict_tool_groups() + public void Catalog_exposes_six_strict_tool_groups() { - Assert.Equal(["file_search", "file_read", "file_edit", "process_run", "task_update"], InternalToolCatalog.All.Select(x => x.Name)); + Assert.Equal(["file_search", "file_read", "file_edit", "process_run", "task_update", "dir"], InternalToolCatalog.All.Select(x => x.Name)); foreach (InternalToolDefinition tool in InternalToolCatalog.All) Assert.False(tool.Parameters.GetProperty("additionalProperties").GetBoolean()); } @@ -86,4 +86,40 @@ public sealed class InternalToolTests handler.EndTask(scope); Assert.Throws<InvalidOperationException>(() => handler.Execute(scope, add.RootElement)); } + + [Theory] + [InlineData("dir", "{}")] + [InlineData("dir", """{"path":"x","operation":"invalid"}""")] + [InlineData("dir", """{"path":"x","operation":"mkdir","extra":1}""")] + [InlineData("dir", """{"operation":"mkdir"}""")] + public void Dir_malformed_arguments_are_rejected(string tool, string json) + { + using JsonDocument document = JsonDocument.Parse(json); + Assert.Throws<ToolValidationException>(() => InternalToolValidator.Validate(tool, document.RootElement)); + } + + [Fact] + public void Dir_catalog_definition_has_required_fields_and_strict_schema() + { + InternalToolDefinition definition = InternalToolCatalog.Get("dir"); + Assert.Equal("dir", definition.Name); + Assert.False(definition.Parameters.GetProperty("additionalProperties").GetBoolean()); + Assert.Equal(["path", "operation"], definition.Parameters.GetProperty("required").EnumerateArray().Select(r => r.GetString()!).ToArray()); + Assert.Equal(["mkdir", "rmdir", "list"], definition.Parameters.GetProperty("properties").GetProperty("operation").GetProperty("enum").EnumerateArray().Select(v => v.GetString()!).ToArray()); + } + + [Fact] + public async Task Dir_dispatched_to_fake_backend_has_external_effect() + { + FakeInternalToolBackend backend = new(); + InternalToolDispatcher dispatcher = new(new DenyByDefaultPolicy(new HashSet<string>(["dir"], StringComparer.Ordinal)), backend); + using JsonDocument document = JsonDocument.Parse("""{"path":"C:\\tmp\\sub","operation":"mkdir"}"""); + + ToolExecutionResult result = await dispatcher.DispatchAsync(Guid.NewGuid(), "dir", document.RootElement, TestContext.Current.CancellationToken); + + Assert.True(result.Success); + Assert.True(result.ExternalEffect); + Assert.Single(backend.Calls); + Assert.Equal("dir", backend.Calls[0].Name); + } } diff --git a/tests/Agent.Core.Tests/OllamaClientTests.cs b/tests/Agent.Core.Tests/OllamaClientTests.cs index aa98764..94ce8cd 100644 --- a/tests/Agent.Core.Tests/OllamaClientTests.cs +++ b/tests/Agent.Core.Tests/OllamaClientTests.cs @@ -117,7 +117,7 @@ public sealed class OllamaClientTests OllamaResult result = await client.ChatWithToolsAsync([new OllamaMessage("user", "read")], null, InternalToolCatalog.All, TestContext.Current.CancellationToken); using JsonDocument document = JsonDocument.Parse(body!); - Assert.Equal(5, document.RootElement.GetProperty("tools").GetArrayLength()); + Assert.Equal(6, document.RootElement.GetProperty("tools").GetArrayLength()); Assert.All(document.RootElement.GetProperty("tools").EnumerateArray(), tool => Assert.False(tool.GetProperty("function").GetProperty("parameters").GetProperty("additionalProperties").GetBoolean())); OllamaToolCall call = Assert.Single(result.ToolCalls!); diff --git a/tests/Agent.Core.Tests/PromptAndRuntimeTests.cs b/tests/Agent.Core.Tests/PromptAndRuntimeTests.cs index c3c634d..5e682cd 100644 --- a/tests/Agent.Core.Tests/PromptAndRuntimeTests.cs +++ b/tests/Agent.Core.Tests/PromptAndRuntimeTests.cs @@ -104,7 +104,7 @@ public sealed class PromptAndRuntimeTests Assert.Single(backend.Calls); Assert.Equal("file_edit", backend.Calls[0].Name); Assert.Equal(2, ollama.CallCount); - Assert.Equal(5, ollama.ToolCount); + Assert.Equal(6, ollama.ToolCount); } [Fact] |
