Files
Playa/tests/Volcanic.AgenticCore.Extensions.Tests/ClaudeCode/ClaudeArgsTests.cs

165 lines
7.6 KiB
C#

using System.Text.Json;
using Volcanic.AgenticCore.Extensions.ClaudeCode;
namespace Volcanic.AgenticCore.Extensions.Tests.ClaudeCode;
public class ClaudeArgsTests
{
[Fact]
public void Build_with_tools_and_effort_yields_the_exact_argv()
{
var input = new ClaudeArgsInput(
"sonnet", "medium", "/tmp/private/system-prompt.txt",
CatalogPath: "/tmp/private/tools.json", ViolationPath: "/tmp/private/mcp-violation", ReadyPath: "/tmp/private/mcp-ready",
BridgeDllPath: "/opt/ext/Volcanic.AgenticCore.Extensions.ClaudeCode.Bridge.dll", DotnetPath: "/usr/bin/dotnet");
var args = ClaudeArgs.Build(input);
var mcp = ClaudeArgs.BuildMcpConfig(input);
string[] expected =
[
"-p",
"--setting-sources", "",
"--settings", "{\"disableAllHooks\":true,\"autoMemoryEnabled\":false,\"totalTokensReminder\":\"off\",\"enabledPlugins\":{\"agents-md@builtin\":false}}",
"--disable-slash-commands",
"--strict-mcp-config",
"--permission-mode", "dontAsk",
"--no-chrome",
"--no-session-persistence",
"--prompt-suggestions", "false",
"--mcp-config", mcp,
"--tools", "",
"--model", "sonnet",
"--effort", "medium",
"--thinking-display", "summarized",
"--input-format", "stream-json",
"--output-format", "stream-json",
"--include-partial-messages",
"--verbose",
"--system-prompt-file", "/tmp/private/system-prompt.txt",
];
Assert.Equal(expected, args);
var server = JsonSerializer.Deserialize<JsonElement>(mcp).GetProperty("mcpServers").GetProperty("volcanic");
Assert.Equal("/usr/bin/dotnet", server.GetProperty("command").GetString());
Assert.Equal(["/opt/ext/Volcanic.AgenticCore.Extensions.ClaudeCode.Bridge.dll"], server.GetProperty("args").EnumerateArray().Select(a => a.GetString()!).ToArray());
var env = server.GetProperty("env");
Assert.Equal("/tmp/private/tools.json", env.GetProperty("VOLCANIC_CLAUDE_TOOL_CATALOG").GetString());
Assert.Equal("/tmp/private/mcp-violation", env.GetProperty("VOLCANIC_CLAUDE_TOOL_VIOLATION").GetString());
Assert.Equal("/tmp/private/mcp-ready", env.GetProperty("VOLCANIC_CLAUDE_TOOL_READY").GetString());
}
[Fact]
public void Build_without_tools_or_effort_uses_an_empty_mcp_config_and_omits_effort()
{
var args = ClaudeArgs.Build(new ClaudeArgsInput("haiku", null, "/tmp/p/system-prompt.txt"));
string[] expected =
[
"-p", "--setting-sources", "", "--settings", ClaudeArgs.Settings, "--disable-slash-commands", "--strict-mcp-config",
"--permission-mode", "dontAsk", "--no-chrome", "--no-session-persistence", "--prompt-suggestions", "false",
"--mcp-config", "{\"mcpServers\":{}}", "--tools", "", "--model", "haiku",
"--thinking-display", "summarized", "--input-format", "stream-json", "--output-format", "stream-json",
"--include-partial-messages", "--verbose", "--system-prompt-file", "/tmp/p/system-prompt.txt",
];
Assert.Equal(expected, args);
Assert.DoesNotContain("--effort", args);
}
[Fact]
public void Mcp_config_honours_the_server_name_and_requires_bridge_and_dotnet()
{
var mcp = ClaudeArgs.BuildMcpConfig(new ClaudeArgsInput("sonnet", null, "/p/s.txt", CatalogPath: "/p/tools.json", BridgeDllPath: "/b.dll", DotnetPath: "dotnet", ServerName: "pi"));
Assert.True(JsonSerializer.Deserialize<JsonElement>(mcp).GetProperty("mcpServers").TryGetProperty("pi", out _));
var missingBridge = Assert.Throws<ClaudeCodeException>(() => ClaudeArgs.BuildMcpConfig(new ClaudeArgsInput("sonnet", null, "/p/s.txt", CatalogPath: "/p/tools.json", DotnetPath: "dotnet")));
Assert.Equal("mcp_startup", missingBridge.Code);
}
[Fact]
public void BuildEnvironment_allow_lists_forces_and_rejects()
{
var parent = new Dictionary<string, string?>
{
["HOME"] = "/home/me",
["PATH"] = "/usr/bin",
["LANG"] = "C.UTF-8",
["LC_ALL"] = "C",
["LC_CTYPE"] = "C.UTF-8",
["http_proxy"] = "http://proxy",
["CLAUDE_CONFIG_DIR"] = "/cfg",
["SECRET_TOKEN"] = "nope",
["NPM_TOKEN"] = "nope",
["DISABLE_COMPACT"] = "0",
["CLAUDE_CODE_MAX_OUTPUT_TOKENS"] = "1",
["NULL_VALUE"] = null,
};
var env = ClaudeArgs.BuildEnvironment(parent, 64_000);
Assert.Equal("/home/me", env["HOME"]);
Assert.Equal("/usr/bin", env["PATH"]);
Assert.Equal("C", env["LC_ALL"]);
Assert.Equal("C.UTF-8", env["LC_CTYPE"]);
Assert.Equal("http://proxy", env["http_proxy"]);
Assert.Equal("/cfg", env["CLAUDE_CONFIG_DIR"]);
Assert.False(env.ContainsKey("SECRET_TOKEN"));
Assert.False(env.ContainsKey("NPM_TOKEN"));
Assert.False(env.ContainsKey("NULL_VALUE"));
Assert.Equal("1", env["CLAUDE_CODE_DISABLE_AUTO_MEMORY"]);
Assert.Equal("1", env["CLAUDE_CODE_DISABLE_GIT_INSTRUCTIONS"]);
Assert.Equal("1", env["CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC"]);
Assert.Equal("1", env["DISABLE_COMPACT"]);
Assert.Equal("64000", env["CLAUDE_CODE_MAX_OUTPUT_TOKENS"]);
}
[Theory]
[InlineData("ANTHROPIC_API_KEY")]
[InlineData("ANTHROPIC_AUTH_TOKEN")]
[InlineData("ANTHROPIC_BASE_URL")]
[InlineData("CLAUDE_CODE_OAUTH_TOKEN")]
[InlineData("CLAUDE_CODE_USE_BEDROCK")]
[InlineData("claude_code_use_vertex")]
public void BuildEnvironment_rejects_api_key_and_third_party_auth(string name)
{
var parent = new Dictionary<string, string?> { ["HOME"] = "/home/me", [name] = "value" };
var ex = Assert.Throws<ClaudeCodeException>(() => ClaudeArgs.BuildEnvironment(parent, 1000));
Assert.Equal("auth", ex.Code);
Assert.Contains(name, ex.Message);
Assert.Equal(name, ClaudeArgs.FindDeniedVariable(parent));
}
[Fact]
public void BuildEnvironment_sets_TZ_from_the_zone_id()
{
if (OperatingSystem.IsWindows()) return; // Windows ignores TZ; see BuildEnvironment_never_sets_TZ_on_Windows
var env = ClaudeArgs.BuildEnvironment(new Dictionary<string, string?> { ["PATH"] = "/usr/bin" }, 1000, "Asia/Tokyo");
Assert.Equal("Asia/Tokyo", env["TZ"]);
}
[Fact]
public void BuildEnvironment_without_a_zone_leaves_TZ_unset_and_never_copies_the_parents()
{
var parent = new Dictionary<string, string?> { ["PATH"] = "/usr/bin", ["TZ"] = "America/Chicago" };
Assert.False(ClaudeArgs.BuildEnvironment(parent, 1000).ContainsKey("TZ"));
Assert.False(ClaudeArgs.AllowListed(parent, null).ContainsKey("TZ"));
var withZone = ClaudeArgs.BuildEnvironment(parent, 1000, "Asia/Tokyo");
Assert.NotEqual("America/Chicago", withZone.GetValueOrDefault("TZ"));
}
[Fact]
public void BuildEnvironment_never_sets_TZ_on_Windows()
{
if (!OperatingSystem.IsWindows()) return;
Assert.False(ClaudeArgs.BuildEnvironment(new Dictionary<string, string?>(), 1000, "Asia/Tokyo").ContainsKey("TZ"));
}
[Fact]
public void IanaZoneId_converts_and_rejects_Local()
{
Assert.Equal("Asia/Tokyo", ClaudeArgs.IanaZoneId(TimeZoneInfo.FindSystemTimeZoneById("Asia/Tokyo")));
Assert.Equal("Asia/Tokyo", ClaudeArgs.IanaZoneId(TimeZoneInfo.FindSystemTimeZoneById("Tokyo Standard Time")));
Assert.Null(ClaudeArgs.IanaZoneId(TimeZoneInfo.CreateCustomTimeZone("Local", TimeSpan.FromHours(1), "Local", "Local")));
}
}