?? ? ? ?? ?? ?? ??? ???? 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:
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user