Test: der Claude-Code-Weg in der echten Sandbox, mit Stellvertreter (#712)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-09-25 23:07:21 +02:00
co-authored by Claude Opus 5.5
parent 7a8852fe6b
commit 4940fdefb0
@@ -186,6 +186,65 @@ public sealed class McpSocketTests : IDisposable
outcome.Text.ShouldContain("added: result.txt");
}
/// <summary>
/// #712: The Claude Code path in the real sandbox, with a stand-in for <c>claude</c> —
/// the real one does not run on the test machine, and a test has no key. The stand-in
/// checks what Claude Code would be given (arguments, key, home, MCP configuration),
/// reaches the API host through the proxy, calls the MCP server the way Claude Code
/// would (the command from mcp.json) and answers in stream-json.
/// </summary>
[ForeignRunFact]
public async Task The_claude_code_path_holds_together_in_the_real_sandbox()
{
await using var host = NewHost();
await using var web = InstanceHttpHost.TryStart(host, NullLoggerFactory.Instance);
web.ShouldNotBeNull();
var bin = Path.Combine(_root, "fake-claude");
Directory.CreateDirectory(bin);
var claude = Path.Combine(bin, "claude");
File.WriteAllText(claude, string.Join("\n",
"#!/bin/sh",
"""case " $* " in *dangerously*) echo "bypass passed" >&2; exit 9;; esac""",
"""[ -n "$ANTHROPIC_API_KEY" ] || { echo "no key" >&2; exit 8; }""",
"""[ -w "$HOME/.claude/settings.json" ] || { echo "no home" >&2; exit 7; }""",
"""prompt=$(cat)""",
"""api=$(curl -sS -o /dev/null -w '%{http_code}' --max-time 20 https://api.anthropic.com/ 2>&1)""",
"""cmd=$(sed -n 's/.*"command":"\([^"]*\)".*/\1/p' /run/agent/mcp.json)""",
"""tools=$(printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | CLAWD_MCP_TOKEN_FILE=/run/agent/mcp-token "$cmd" --mcp-bridge --socket /run/mcp.sock 2>&1 | tr -d '"')""",
"""echo "changed by claude" > /work/answer.txt""",
"""printf '%s\n' '{"type":"system","subtype":"init","model":"claude-test"}'""",
"""printf '{"type":"result","subtype":"success","is_error":false,"num_turns":1,"result":"prompt=%s api=%s tools=%s","total_cost_usd":0.01,"usage":{"input_tokens":10,"output_tokens":5}}\n' "$prompt" "$api" "$tools" """,
""));
if (OperatingSystem.IsLinux())
File.SetUnixFileMode(claude, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute);
var sandbox = (Core.Sandbox.BwrapSandbox)Core.Sandbox.BwrapSandbox.Detect(NullLogger.Instance);
var helper = Core.Sandbox.SandboxHelper.For(ForeignRunFactAttribute.HelperPath!);
sandbox.Helper = helper;
var settings = new Core.Runtimes.ClaudeCodeSettings { Executable = claude, ApiKey = "sk-ant-fake" };
var runner = new Core.Runtimes.ForeignRunner(sandbox, [new Core.Runtimes.ClaudeCodeRuntime(() => settings)], host.Mcp,
InstanceEndpoint.McpSocketPath(host.InstancePath), helper, NullLogger.Instance);
var agentDir = Path.Combine(host.InstancePath, "Agents", "Agent-coder");
Directory.CreateDirectory(Path.Combine(agentDir, "Workspace"));
var agent = new AgentConfig
{
AgentId = "coder", AgentDir = agentDir, WorkspacePath = Path.Combine(agentDir, "Workspace"),
Runtime = new Core.Runtimes.AgentRuntimeConfig { Kind = "claude-code", TimeoutMinutes = 2 }
};
var outcome = await runner.RunAsync(new Core.Runtimes.ForeignRunRequest(agent, "run-cc", null, "brief", "fix it"), _ => { }, CancellationToken.None);
outcome.Status.ShouldBe(AgentRunStatus.Completed, outcome.Text);
outcome.Text!.ShouldContain("prompt=fix it");
outcome.Text.ShouldNotContain("api=000", customMessage: "the API host is reached through the proxy");
outcome.Text.ShouldContain("name:Echo");
outcome.Text.ShouldContain("added: answer.txt");
outcome.Model.ShouldBe("claude-test");
outcome.CostUsd.ShouldBe(0.01m);
}
private sealed class EchoTool : IAgentTool
{
public string Name => "Echo";