Skip to content

Commit 52c90fa

Browse files
committed
Release 0.1.1 and sync Claude Code upstream reference
1 parent e395340 commit 52c90fa

35 files changed

+1162
-379
lines changed

AGENTS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ If no new rule is detected -> do not update the file.
157157
- Never add fake fallback calls/mocks in production paths; unsupported runtime cases must fail explicitly with actionable errors.
158158
- No magic literals: extract constants/enums/config values.
159159
- In SDK production code, do not inline string literals in implementation logic; promote them to named constants (paths, env vars, command names, switch/comparison tokens) for reviewability and consistency.
160+
- Outside constant declarations themselves, do not use inline string literals in C# code; every implementation/test string value must be routed through a named constant for consistency during review and refactoring.
160161
- Do not inline filesystem/path segment string literals in implementation logic; define named constants and reuse them.
161162
- Never override or silently mutate explicit user-provided Claude Code CLI settings (for example `web_search=disabled`); pass through user intent exactly.
162163
- Protocol and CLI string tokens are mandatory constants: never inline literals in parsing, mapping, or switch branches.
@@ -165,6 +166,7 @@ If no new rule is detected -> do not update the file.
165166
- Keep public API and naming aligned with package/namespace `ManagedCode.ClaudeCodeSharpSDK`.
166167
- Solution/workspace file naming must use `ManagedCode.ClaudeCodeSharpSDK` prefix for consistency with package identity.
167168
- Keep package/version metadata centralized in `Directory.Build.props`; avoid duplicating version structure or release metadata blocks in individual `.csproj` files unless a project-specific override is required.
169+
- Do not bump package/release version for PRs that only change tests or submodule-backed test/reference material and do not touch SDK production projects (`ClaudeCodeSharpSDK*` runtime code); merge/commit such changes without creating a new release version.
168170
- Never hardcode guessed Claude/Anthropic model names in tests, docs, or defaults; verify supported models and active default via Claude Code CLI first.
169171
- Before setting or changing any `Model` value, read available models and current default from the local `claude` CLI in the same environment/account and only then update code/tests/docs.
170172
- Model identifiers in code/tests must come from centralized constants or a shared resolver helper; do not inline model string literals repeatedly.

ClaudeCodeSharpSDK.Extensions.AI/Internal/ChatOptionsMapper.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ namespace ManagedCode.ClaudeCodeSharpSDK.Extensions.AI.Internal;
77

88
internal static class ChatOptionsMapper
99
{
10+
private const string InvalidValueMessagePrefix = "Invalid value for Claude chat option";
11+
private const string Space = " ";
12+
private const string MessageQuote = "'";
13+
private const string MessageSuffix = ".";
14+
1015
internal const string WorkingDirectoryKey = "claude:working_directory";
1116
internal const string PermissionModeKey = "claude:permission_mode";
1217
internal const string AllowedToolsKey = "claude:allowed_tools";
@@ -231,6 +236,6 @@ when decimal.TryParse(jsonString.GetString(), NumberStyles.Number, CultureInfo.I
231236

232237
private static InvalidOperationException CreateInvalidValueException(string key)
233238
{
234-
return new InvalidOperationException($"Invalid value for Claude chat option '{key}'.");
239+
return new InvalidOperationException(string.Concat(InvalidValueMessagePrefix, Space, MessageQuote, key, MessageQuote, MessageSuffix));
235240
}
236241
}
Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
11
using System.Runtime.CompilerServices;
22

3-
[assembly: InternalsVisibleTo("ManagedCode.ClaudeCodeSharpSDK.Tests")]
3+
[assembly: InternalsVisibleTo(AssemblyNames.TestsAssemblyName)]
4+
5+
internal static class AssemblyNames
6+
{
7+
internal const string TestsAssemblyName = "ManagedCode.ClaudeCodeSharpSDK.Tests";
8+
}

ClaudeCodeSharpSDK.Tests/Integration/ClaudeCliSmokeTests.cs

Lines changed: 69 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
using System.Diagnostics;
22
using System.Text.Json;
33
using ManagedCode.ClaudeCodeSharpSDK.Internal;
4+
using ManagedCode.ClaudeCodeSharpSDK.Tests.Shared;
45

56
namespace ManagedCode.ClaudeCodeSharpSDK.Tests.Integration;
67

78
public class ClaudeCliSmokeTests
89
{
9-
private const string SolutionFileName = "ManagedCode.ClaudeCodeSharpSDK.slnx";
10-
private const string SandboxDirectoryName = ".sandbox";
1110
private const string SandboxPrefix = "ClaudeCliSmokeTests-";
1211
private const string HomeEnvironmentVariable = "HOME";
1312
private const string UserProfileEnvironmentVariable = "USERPROFILE";
@@ -17,6 +16,31 @@ public class ClaudeCliSmokeTests
1716
private const string ClaudeConfigDirEnvironmentVariable = "CLAUDE_CONFIG_DIR";
1817
private const string AnthropicApiKeyEnvironmentVariable = "ANTHROPIC_API_KEY";
1918
private const string AnthropicBaseUrlEnvironmentVariable = "ANTHROPIC_BASE_URL";
19+
private const string VersionFlag = "--version";
20+
private const string HelpFlag = "--help";
21+
private const string PrintFlag = "-p";
22+
private const string OutputFormatFlag = "--output-format";
23+
private const string StreamJsonFormat = "stream-json";
24+
private const string VerboseFlag = "--verbose";
25+
private const string ReplyWithOkPrompt = "Reply with ok";
26+
private const string ClaudeCodeFragment = "Claude Code";
27+
private const string LoginGuidanceFragment = "Please run /login";
28+
private const string ErrorJsonFragment = "\"is_error\":true";
29+
private const string TypePropertyName = "type";
30+
private const string SystemType = "system";
31+
private const string ResultType = "result";
32+
private const string ClaudeConfigDirectoryName = ".claude";
33+
private const string ConfigDirectoryName = ".config";
34+
private const string NewLine = "\n";
35+
private const string CarriageReturn = "\r";
36+
private const string PathRootedButMissingMessagePrefix = "Claude Code CLI path is rooted but missing:";
37+
private const string FailedToResolveExecutablePathMessage = "Failed to resolve Claude Code CLI path.";
38+
private const string CouldNotLocateRepositoryRootMessage = "Could not locate repository root from test execution directory.";
39+
private const string StartProcessFailedMessagePrefix = "Failed to start Claude Code CLI at";
40+
private const string Space = " ";
41+
private const string MessageQuote = "'";
42+
private const string MessageSuffix = ".";
43+
private static readonly string[] StandardLineSeparators = [Environment.NewLine, NewLine, CarriageReturn];
2044

2145
[Test]
2246
public async Task ClaudeCli_Smoke_FindExecutablePath_ResolvesExistingBinary()
@@ -28,21 +52,21 @@ public async Task ClaudeCli_Smoke_FindExecutablePath_ResolvesExistingBinary()
2852
[Test]
2953
public async Task ClaudeCli_Smoke_VersionCommand_ReturnsClaudeCodeVersion()
3054
{
31-
var result = await RunClaudeAsync(ResolveExecutablePath(), null, "--version");
55+
var result = await RunClaudeAsync(ResolveExecutablePath(), null, VersionFlag);
3256

3357
await Assert.That(result.ExitCode).IsEqualTo(0);
3458
await Assert.That(string.Concat(result.StandardOutput, result.StandardError))
35-
.Contains("Claude Code");
59+
.Contains(ClaudeCodeFragment);
3660
}
3761

3862
[Test]
3963
public async Task ClaudeCli_Smoke_HelpCommand_DescribesStreamJsonOutput()
4064
{
41-
var result = await RunClaudeAsync(ResolveExecutablePath(), null, "--help");
65+
var result = await RunClaudeAsync(ResolveExecutablePath(), null, HelpFlag);
4266

4367
await Assert.That(result.ExitCode).IsEqualTo(0);
4468
await Assert.That(string.Concat(result.StandardOutput, result.StandardError))
45-
.Contains("--output-format");
69+
.Contains(OutputFormatFlag);
4670
}
4771

4872
[Test]
@@ -55,24 +79,24 @@ public async Task ClaudeCli_Smoke_PrintModeWithoutAuth_EmitsInitAndLoginGuidance
5579
var result = await RunClaudeAsync(
5680
ResolveExecutablePath(),
5781
CreateUnauthenticatedEnvironmentOverrides(sandboxDirectory),
58-
"-p",
59-
"--output-format",
60-
"stream-json",
61-
"--verbose",
62-
"Reply with ok");
82+
PrintFlag,
83+
OutputFormatFlag,
84+
StreamJsonFormat,
85+
VerboseFlag,
86+
ReplyWithOkPrompt);
6387

6488
var stdoutLines = result.StandardOutput
65-
.Split([Environment.NewLine, "\n", "\r"], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
89+
.Split(StandardLineSeparators, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
6690

6791
await Assert.That(stdoutLines.Length).IsGreaterThanOrEqualTo(2);
68-
await Assert.That(result.StandardOutput).Contains("Please run /login");
69-
await Assert.That(result.StandardOutput).Contains("\"is_error\":true");
92+
await Assert.That(result.StandardOutput).Contains(LoginGuidanceFragment);
93+
await Assert.That(result.StandardOutput).Contains(ErrorJsonFragment);
7094

7195
using var initDocument = JsonDocument.Parse(stdoutLines[0]);
7296
using var finalDocument = JsonDocument.Parse(stdoutLines[^1]);
7397

74-
await Assert.That(initDocument.RootElement.GetProperty("type").GetString()).IsEqualTo("system");
75-
await Assert.That(finalDocument.RootElement.GetProperty("type").GetString()).IsEqualTo("result");
98+
await Assert.That(initDocument.RootElement.GetProperty(TypePropertyName).GetString()).IsEqualTo(SystemType);
99+
await Assert.That(finalDocument.RootElement.GetProperty(TypePropertyName).GetString()).IsEqualTo(ResultType);
76100
}
77101
finally
78102
{
@@ -90,37 +114,44 @@ private static string ResolveExecutablePath()
90114
return resolvedPath;
91115
}
92116

93-
throw new InvalidOperationException($"Claude Code CLI path is rooted but missing: '{resolvedPath}'.");
117+
throw new InvalidOperationException(
118+
string.Concat(
119+
PathRootedButMissingMessagePrefix,
120+
Space,
121+
MessageQuote,
122+
resolvedPath,
123+
MessageQuote,
124+
MessageSuffix));
94125
}
95126

96127
if (ClaudeCliLocator.TryResolvePathExecutable(
97-
Environment.GetEnvironmentVariable("PATH"),
128+
Environment.GetEnvironmentVariable(TestConstants.PathEnvironmentVariable),
98129
OperatingSystem.IsWindows(),
99130
out var pathExecutable))
100131
{
101132
return pathExecutable;
102133
}
103134

104-
throw new InvalidOperationException("Failed to resolve Claude Code CLI path.");
135+
throw new InvalidOperationException(FailedToResolveExecutablePathMessage);
105136
}
106137

107138
private static string CreateSandboxDirectory()
108139
{
109140
var sandboxDirectory = Path.Combine(
110141
ResolveRepositoryRootPath(),
111-
"tests",
112-
SandboxDirectoryName,
113-
$"{SandboxPrefix}{Guid.NewGuid():N}");
142+
TestConstants.TestsDirectoryName,
143+
TestConstants.SandboxDirectoryName,
144+
string.Concat(SandboxPrefix, Guid.NewGuid().ToString(TestConstants.NumericGuidFormat)));
114145
Directory.CreateDirectory(sandboxDirectory);
115146
return sandboxDirectory;
116147
}
117148

118149
private static Dictionary<string, string> CreateUnauthenticatedEnvironmentOverrides(string sandboxDirectory)
119150
{
120-
var claudeConfigDirectory = Path.Combine(sandboxDirectory, ".claude");
121-
var configHome = Path.Combine(sandboxDirectory, ".config");
122-
var appData = Path.Combine(sandboxDirectory, "AppData", "Roaming");
123-
var localAppData = Path.Combine(sandboxDirectory, "AppData", "Local");
151+
var claudeConfigDirectory = Path.Combine(sandboxDirectory, ClaudeConfigDirectoryName);
152+
var configHome = Path.Combine(sandboxDirectory, ConfigDirectoryName);
153+
var appData = Path.Combine(sandboxDirectory, TestConstants.AppDataDirectoryName, TestConstants.RoamingDirectoryName);
154+
var localAppData = Path.Combine(sandboxDirectory, TestConstants.AppDataDirectoryName, TestConstants.LocalDirectoryName);
124155

125156
Directory.CreateDirectory(claudeConfigDirectory);
126157
Directory.CreateDirectory(configHome);
@@ -135,8 +166,8 @@ private static Dictionary<string, string> CreateUnauthenticatedEnvironmentOverri
135166
[AppDataEnvironmentVariable] = appData,
136167
[LocalAppDataEnvironmentVariable] = localAppData,
137168
[ClaudeConfigDirEnvironmentVariable] = claudeConfigDirectory,
138-
[AnthropicApiKeyEnvironmentVariable] = string.Empty,
139-
[AnthropicBaseUrlEnvironmentVariable] = string.Empty,
169+
[AnthropicApiKeyEnvironmentVariable] = TestConstants.EmptyString,
170+
[AnthropicBaseUrlEnvironmentVariable] = TestConstants.EmptyString,
140171
};
141172
}
142173

@@ -145,15 +176,15 @@ private static string ResolveRepositoryRootPath()
145176
var current = new DirectoryInfo(AppContext.BaseDirectory);
146177
while (current is not null)
147178
{
148-
if (File.Exists(Path.Combine(current.FullName, SolutionFileName)))
179+
if (File.Exists(Path.Combine(current.FullName, TestConstants.SolutionFileName)))
149180
{
150181
return current.FullName;
151182
}
152183

153184
current = current.Parent;
154185
}
155186

156-
throw new InvalidOperationException("Could not locate repository root from test execution directory.");
187+
throw new InvalidOperationException(CouldNotLocateRepositoryRootMessage);
157188
}
158189

159190
private static async Task<ClaudeProcessResult> RunClaudeAsync(
@@ -185,7 +216,14 @@ private static async Task<ClaudeProcessResult> RunClaudeAsync(
185216
using var process = new Process { StartInfo = startInfo };
186217
if (!process.Start())
187218
{
188-
throw new InvalidOperationException($"Failed to start Claude Code CLI at '{executablePath}'.");
219+
throw new InvalidOperationException(
220+
string.Concat(
221+
StartProcessFailedMessagePrefix,
222+
Space,
223+
MessageQuote,
224+
executablePath,
225+
MessageQuote,
226+
MessageSuffix));
189227
}
190228

191229
var standardOutputTask = process.StandardOutput.ReadToEndAsync();

ClaudeCodeSharpSDK.Tests/Integration/RealClaudeIntegrationTests.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33

44
namespace ManagedCode.ClaudeCodeSharpSDK.Tests.Integration;
55

6-
[Property("RequiresClaudeAuth", "true")]
6+
[Property(TestConstants.RequiresClaudeAuthPropertyName, TestConstants.TrueString)]
77
[RequiresAuthenticatedClaude]
88
public class RealClaudeIntegrationTests
99
{
10+
private const string ReplyWithOkOnlyPrompt = "Reply with OK only.";
11+
1012
[Test]
1113
public async Task RealClaude_RunAsync_WhenAuthenticated_ReturnsResponse()
1214
{
@@ -21,7 +23,7 @@ public async Task RealClaude_RunAsync_WhenAuthenticated_ReturnsResponse()
2123
NoSessionPersistence = true,
2224
});
2325

24-
var result = await thread.RunAsync("Reply with OK only.");
26+
var result = await thread.RunAsync(ReplyWithOkOnlyPrompt);
2527

2628
await Assert.That(string.IsNullOrWhiteSpace(result.FinalResponse)).IsFalse();
2729
}

ClaudeCodeSharpSDK.Tests/MEAI/ChatMessageMapperTests.cs

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,30 @@ namespace ManagedCode.ClaudeCodeSharpSDK.Extensions.AI.Tests;
55

66
public class ChatMessageMapperTests
77
{
8+
private const string SystemPrompt = "Be concise";
9+
private const string FirstQuestion = "First question";
10+
private const string FirstAnswer = "First answer";
11+
private const string FollowUp = "Follow up";
12+
private const string ExtraContext = "With extra context";
13+
private const string ExpectedPrompt = "[System] Be concise\n\nFirst question\n\n[Assistant] First answer\n\nFollow up\n\nWith extra context";
14+
private const string DescribeImagePrompt = "Describe this image";
15+
private const string PngMimeType = "image/png";
16+
private const string TextOnlyPromptsMessage = "text-only prompts";
17+
818
[Test]
919
public async Task ToClaudeInput_MixedConversation_PreservesChronology()
1020
{
1121
var messages = new[]
1222
{
13-
new ChatMessage(ChatRole.System, "Be concise"),
14-
new ChatMessage(ChatRole.User, "First question"),
15-
new ChatMessage(ChatRole.Assistant, "First answer"),
16-
new ChatMessage(ChatRole.User, [new TextContent("Follow up"), new TextContent("With extra context")]),
23+
new ChatMessage(ChatRole.System, SystemPrompt),
24+
new ChatMessage(ChatRole.User, FirstQuestion),
25+
new ChatMessage(ChatRole.Assistant, FirstAnswer),
26+
new ChatMessage(ChatRole.User, [new TextContent(FollowUp), new TextContent(ExtraContext)]),
1727
};
1828

1929
var prompt = ChatMessageMapper.ToClaudeInput(messages);
2030

21-
await Assert.That(prompt).IsEqualTo(
22-
"[System] Be concise\n\nFirst question\n\n[Assistant] First answer\n\nFollow up\n\nWith extra context");
31+
await Assert.That(prompt).IsEqualTo(ExpectedPrompt);
2332
}
2433

2534
[Test]
@@ -42,13 +51,13 @@ public async Task ToClaudeInput_ImageContent_ThrowsNotSupportedException()
4251
new ChatMessage(
4352
ChatRole.User,
4453
[
45-
new TextContent("Describe this image"),
46-
new DataContent(new byte[] { 0x89, 0x50, 0x4E, 0x47 }, "image/png"),
54+
new TextContent(DescribeImagePrompt),
55+
new DataContent(new byte[] { 0x89, 0x50, 0x4E, 0x47 }, PngMimeType),
4756
]),
4857
};
4958

5059
var exception = await Assert.That(() => ChatMessageMapper.ToClaudeInput(messages)).ThrowsException();
5160
await Assert.That(exception).IsTypeOf<NotSupportedException>();
52-
await Assert.That(exception!.Message).Contains("text-only prompts");
61+
await Assert.That(exception!.Message).Contains(TextOnlyPromptsMessage);
5362
}
5463
}

0 commit comments

Comments
 (0)