?? ? ? ?? ?? ?? ??? ???? file_write ?? ??? ??

? ?? ???? folder_map, grep, file_read, env_tool, skill_manager, mcp_list_resources ?? ?? ???? ??? ??? AgentLoopCodeRuntimeGuards? ????, ?? ??(C:\ ?) fallback? ?? ? file_write ?? ?? ??? ????? ????.

Code ??? ?? ?? ???? meta tool? ????, direct-creation ????? ?? ??? ?? ??? ?? ???? file_write ?? ???? ????? AgentLoopService? SystemPromptBuilder? ????. ???? ?? <tool_call>? AgentLoopResponseClassificationService?? ??? file_write ??? ???.

AgentLoopE2ETests? AgentLoopResponseClassificationServiceTests? ??? ? ?? ?? ?? fallback ??, skill_manager detour ??, text-embedded file_write ??? ??? ???? README.md? docs/DEVELOPMENT.md? 2026-04-15 14:00 (KST) ?? ??? ????.

??:
- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_empty_workspace_fix2\\ -p:IntermediateOutputPath=obj\\verify_empty_workspace_fix2\\ (?? 0 / ?? 0)
- dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "FullyQualifiedName~RunAsync_EmptyWorkspace_BlocksExternalFallbackAndRecoversToFileWrite|FullyQualifiedName~RunAsync_EmptyWorkspace_DisallowsSkillManagerAndRecoversToFileWrite|FullyQualifiedName~RunAsync_TextEmbeddedToolCall_RecoversAndExecutesFileWrite|FullyQualifiedName~Classify_ShouldRecoverToolCallEmbeddedInText" -p:OutputPath=bin\\verify_empty_workspace_fix2_tests\\ -p:IntermediateOutputPath=obj\\verify_empty_workspace_fix2_tests\\ (?? 4)
This commit is contained in:
2026-04-15 14:02:23 +09:00
parent f3a31e97b1
commit 4403dc3fc3
10 changed files with 513 additions and 1 deletions

View File

@@ -507,6 +507,136 @@ public class AgentLoopE2ETests
events.Should().Contain(e => e.Type == AgentEventType.ToolResult && e.ToolName == "math_eval" && e.Success);
}
[Fact]
public async Task RunAsync_EmptyWorkspace_BlocksExternalFallbackAndRecoversToFileWrite()
{
var tempDir = Path.Combine(Path.GetTempPath(), "axcopilot-empty-workspace-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(tempDir);
try
{
using var server = new FakeOllamaServer(
[
BuildToolCallResponse("folder_map", new { path = ".", depth = 2, include_files = true }, "empty workspace check"),
BuildToolCallResponse("grep", new { pattern = "\\.html$", path = @"C:\\", files_only = true }, "bad external fallback"),
BuildToolCallResponse("file_write", new { path = "index.html", content = "<html><body>clock</body></html>" }, "create file directly"),
BuildTextResponse("완료"),
]);
var settings = BuildLoopSettings(server.Endpoint);
settings.Settings.Llm.WorkFolder = tempDir;
using var llm = new LlmService(settings);
using var tools = ToolRegistry.CreateDefault();
var loop = new AgentLoopService(llm, tools, settings) { ActiveTab = "Code" };
var events = new List<AgentEvent>();
loop.EventOccurred += evt => events.Add(evt);
var result = await loop.RunAsync(
[
new ChatMessage { Role = "user", Content = "실시간으로 시간을 표시하는 웹페이지 만들어줘" }
]);
result.Should().Contain("완료");
File.Exists(Path.Combine(tempDir, "index.html")).Should().BeTrue();
events.Should().Contain(e =>
e.Type == AgentEventType.Error
&& (e.ToolName == "grep" || e.ToolName == "folder_map")
&& e.Summary.Contains("Empty workspace guard blocked", StringComparison.OrdinalIgnoreCase));
events.Should().Contain(e => e.Type == AgentEventType.ToolCall && e.ToolName == "file_write");
events.Should().NotContain(e => e.Type == AgentEventType.PermissionRequest && e.ToolName == "grep");
}
finally
{
try { if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true); } catch { }
}
}
[Fact]
public async Task RunAsync_EmptyWorkspace_DisallowsSkillManagerAndRecoversToFileWrite()
{
var tempDir = Path.Combine(Path.GetTempPath(), "axcopilot-empty-workspace-skill-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(tempDir);
try
{
using var server = new FakeOllamaServer(
[
BuildToolCallResponse("folder_map", new { path = ".", depth = 2, include_files = true }, "empty workspace check"),
BuildToolCallResponse("skill_manager", new { action = "list" }, "irrelevant meta tool"),
BuildToolCallResponse("file_write", new { path = "index.html", content = "<html><body>ok</body></html>" }, "create file"),
BuildTextResponse("완료"),
]);
var settings = BuildLoopSettings(server.Endpoint);
settings.Settings.Llm.WorkFolder = tempDir;
using var llm = new LlmService(settings);
using var tools = ToolRegistry.CreateDefault();
var loop = new AgentLoopService(llm, tools, settings) { ActiveTab = "Code" };
var events = new List<AgentEvent>();
loop.EventOccurred += evt => events.Add(evt);
var result = await loop.RunAsync(
[
new ChatMessage { Role = "user", Content = "빈 폴더에 index.html 만들어줘" }
]);
result.Should().Contain("완료");
File.Exists(Path.Combine(tempDir, "index.html")).Should().BeTrue();
events.Should().Contain(e => e.Type == AgentEventType.Error && e.ToolName == "skill_manager");
events.Should().Contain(e => e.Type == AgentEventType.ToolCall && e.ToolName == "file_write");
}
finally
{
try { if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true); } catch { }
}
}
[Fact]
public async Task RunAsync_TextEmbeddedToolCall_RecoversAndExecutesFileWrite()
{
var tempDir = Path.Combine(Path.GetTempPath(), "axcopilot-text-toolcall-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(tempDir);
try
{
using var server = new FakeOllamaServer(
[
BuildRawResponse("""
{
"message": {
"content": "<tool_call>{\"name\":\"file_write\",\"arguments\":{\"path\":\"index.html\",\"content\":\"<html><body>clock</body></html>\"}}</tool_call>\n파일을 작성합니다."
}
}
"""),
BuildTextResponse("완료"),
]);
var settings = BuildLoopSettings(server.Endpoint);
settings.Settings.Llm.WorkFolder = tempDir;
using var llm = new LlmService(settings);
using var tools = ToolRegistry.CreateDefault();
var loop = new AgentLoopService(llm, tools, settings) { ActiveTab = "Code" };
var events = new List<AgentEvent>();
loop.EventOccurred += evt => events.Add(evt);
var result = await loop.RunAsync(
[
new ChatMessage { Role = "user", Content = "index.html 파일 만들어줘" }
]);
result.Should().Contain("완료");
File.Exists(Path.Combine(tempDir, "index.html")).Should().BeTrue();
events.Should().Contain(e => e.Type == AgentEventType.ToolCall && e.ToolName == "file_write");
}
finally
{
try { if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true); } catch { }
}
}
private static SettingsService BuildLoopSettings(string endpoint)
{
var settings = new SettingsService();
@@ -520,8 +650,16 @@ public class AgentLoopE2ETests
settings.Settings.Llm.EnableToolHooks = false;
settings.Settings.Llm.EnableAutoRouter = false;
settings.Settings.Llm.EnableForkSkillDelegationEnforcement = true;
settings.Settings.Llm.EnableProjectSkillDiscovery = false;
settings.Settings.Llm.EnablePluginSkillDiscovery = false;
settings.Settings.Llm.EnableMcpSkillDiscovery = false;
settings.Settings.Llm.EnableLegacyCommandSkills = false;
settings.Settings.Llm.SkillsFolderPath = "";
settings.Settings.Llm.AdditionalSkillFolders.Clear();
settings.Settings.Llm.FilePermission = "BypassPermissions";
settings.Settings.Llm.DefaultAgentPermission = "BypassPermissions";
settings.Settings.Llm.BlockedPaths.Clear();
settings.Settings.Llm.BlockedExtensions.Clear();
return settings;
}
@@ -565,6 +703,9 @@ public class AgentLoopE2ETests
private static string BuildTextOnlyResponse(string content)
=> BuildTextResponse(content);
private static string BuildRawResponse(string rawJson)
=> rawJson;
private sealed class FakeOllamaServer : IDisposable
{
private readonly HttpListener _listener;

View File

@@ -62,4 +62,27 @@ public class AgentLoopResponseClassificationServiceTests
result.BuildThinkingSummary().Should().Be("실제 수정 범위를 다시 확인합니다.");
}
[Fact]
public void Classify_ShouldRecoverToolCallEmbeddedInText()
{
var blocks = new List<ContentBlock>
{
new()
{
Type = "text",
Text = """
<tool_call>{"name":"file_write","arguments":{"path":"index.html","content":"<html>clock</html>"}}</tool_call>
.
"""
}
};
var result = AgentLoopResponseClassificationService.Classify(blocks, consecutiveNoToolResponses: 0);
result.ToolCalls.Should().ContainSingle();
result.ToolCalls[0].ToolName.Should().Be("file_write");
result.TextResponse.Should().Contain("이제 파일을 생성합니다.");
result.TextResponse.Should().NotContain("<tool_call>");
result.NextConsecutiveNoToolResponses.Should().Be(0);
}
}