Skip to content

Commit 57e60e5

Browse files
authored
Merge pull request #5 from viamus/feature/github-actions
Add CI pipeline and unit test project
2 parents e59ac01 + 9f94cca commit 57e60e5

14 files changed

Lines changed: 1670 additions & 0 deletions

.github/workflows/ci.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
build-and-test:
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- uses: actions/checkout@v4
15+
16+
- name: Setup .NET
17+
uses: actions/setup-dotnet@v4
18+
with:
19+
dotnet-version: 10.0.x
20+
dotnet-quality: preview
21+
22+
- name: Restore dependencies
23+
run: dotnet restore
24+
25+
- name: Build
26+
run: dotnet build --no-restore --configuration Release
27+
28+
- name: Test
29+
run: dotnet test --no-build --configuration Release --verbosity normal
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
using CodeGenesis.Engine.Claude;
2+
using FluentAssertions;
3+
4+
namespace CodeGenesis.Engine.Tests.Claude;
5+
6+
public class ClaudeResponseTests
7+
{
8+
private static readonly TimeSpan TestDuration = TimeSpan.FromSeconds(5);
9+
10+
#region FailureKind
11+
12+
[Fact]
13+
public void FailureKind_SuccessResponse_ReturnsNone()
14+
{
15+
var response = new ClaudeResponse { Success = true };
16+
17+
response.FailureKind.Should().Be(ClaudeFailureKind.None);
18+
}
19+
20+
[Theory]
21+
[InlineData("timed out waiting for response")]
22+
[InlineData("Process timed out")]
23+
public void FailureKind_TimeoutWithExitCodeMinus1_ReturnsTimeout(string errorMsg)
24+
{
25+
var response = new ClaudeResponse
26+
{
27+
Success = false,
28+
ExitCode = -1,
29+
ErrorMessage = errorMsg
30+
};
31+
32+
response.FailureKind.Should().Be(ClaudeFailureKind.Timeout);
33+
}
34+
35+
[Fact]
36+
public void FailureKind_TimeoutWithoutExitCodeMinus1_IsNotTimeout()
37+
{
38+
var response = new ClaudeResponse
39+
{
40+
Success = false,
41+
ExitCode = 1,
42+
ErrorMessage = "timed out"
43+
};
44+
45+
response.FailureKind.Should().NotBe(ClaudeFailureKind.Timeout);
46+
}
47+
48+
[Theory]
49+
[InlineData("rate limit exceeded")]
50+
[InlineData("429 Too Many Requests")]
51+
[InlineData("server overloaded")]
52+
[InlineData("too many requests")]
53+
public void FailureKind_RateLimitMessages_ReturnsRateLimit(string errorMsg)
54+
{
55+
var response = new ClaudeResponse
56+
{
57+
Success = false,
58+
ExitCode = 1,
59+
ErrorMessage = errorMsg
60+
};
61+
62+
response.FailureKind.Should().Be(ClaudeFailureKind.RateLimit);
63+
}
64+
65+
[Fact]
66+
public void FailureKind_MaxTurnsMessage_ReturnsMaxTurns()
67+
{
68+
var response = new ClaudeResponse
69+
{
70+
Success = false,
71+
ExitCode = 1,
72+
ErrorMessage = "max_turns reached"
73+
};
74+
75+
response.FailureKind.Should().Be(ClaudeFailureKind.MaxTurns);
76+
}
77+
78+
[Fact]
79+
public void FailureKind_GenericError_ReturnsOther()
80+
{
81+
var response = new ClaudeResponse
82+
{
83+
Success = false,
84+
ExitCode = 1,
85+
ErrorMessage = "something unexpected"
86+
};
87+
88+
response.FailureKind.Should().Be(ClaudeFailureKind.Other);
89+
}
90+
91+
[Fact]
92+
public void FailureKind_NullErrorMessage_ReturnsOther()
93+
{
94+
var response = new ClaudeResponse
95+
{
96+
Success = false,
97+
ExitCode = 1,
98+
ErrorMessage = null
99+
};
100+
101+
response.FailureKind.Should().Be(ClaudeFailureKind.Other);
102+
}
103+
104+
#endregion
105+
106+
#region FromJson
107+
108+
[Fact]
109+
public void FromJson_SuccessResponse_ParsesCorrectly()
110+
{
111+
var json = """
112+
{
113+
"result": "Hello, world!",
114+
"usage": {
115+
"input_tokens": 100,
116+
"output_tokens": 50
117+
},
118+
"total_cost_usd": 0.005,
119+
"num_turns": 1
120+
}
121+
""";
122+
123+
var response = ClaudeResponse.FromJson(json, TestDuration);
124+
125+
response.Success.Should().BeTrue();
126+
response.Result.Should().Be("Hello, world!");
127+
response.InputTokens.Should().Be(100);
128+
response.OutputTokens.Should().Be(50);
129+
response.CostUsd.Should().Be(0.005);
130+
response.Duration.Should().Be(TestDuration);
131+
response.ExitCode.Should().Be(0);
132+
}
133+
134+
[Fact]
135+
public void FromJson_WithCacheTokens_IncludesInInputTotal()
136+
{
137+
var json = """
138+
{
139+
"result": "cached response",
140+
"usage": {
141+
"input_tokens": 100,
142+
"cache_creation_input_tokens": 50,
143+
"cache_read_input_tokens": 25,
144+
"output_tokens": 30
145+
}
146+
}
147+
""";
148+
149+
var response = ClaudeResponse.FromJson(json, TestDuration);
150+
151+
response.Success.Should().BeTrue();
152+
response.InputTokens.Should().Be(175); // 100 + 50 + 25
153+
response.OutputTokens.Should().Be(30);
154+
}
155+
156+
[Fact]
157+
public void FromJson_ErrorMaxTurns_ReturnsFailure()
158+
{
159+
var json = """
160+
{
161+
"subtype": "error_max_turns",
162+
"result": "max turns exceeded",
163+
"usage": {
164+
"input_tokens": 500,
165+
"output_tokens": 200
166+
},
167+
"num_turns": 5
168+
}
169+
""";
170+
171+
var response = ClaudeResponse.FromJson(json, TestDuration);
172+
173+
response.Success.Should().BeFalse();
174+
response.ErrorMessage.Should().Contain("max_turns limit");
175+
response.ErrorMessage.Should().Contain("5 turn(s)");
176+
response.InputTokens.Should().Be(500);
177+
response.OutputTokens.Should().Be(200);
178+
}
179+
180+
[Fact]
181+
public void FromJson_IsError_ReturnsFailure()
182+
{
183+
var json = """
184+
{
185+
"is_error": true,
186+
"result": "Something broke",
187+
"usage": {
188+
"input_tokens": 10,
189+
"output_tokens": 5
190+
}
191+
}
192+
""";
193+
194+
var response = ClaudeResponse.FromJson(json, TestDuration);
195+
196+
response.Success.Should().BeFalse();
197+
response.ErrorMessage.Should().Contain("Something broke");
198+
}
199+
200+
[Fact]
201+
public void FromJson_InvalidJson_ReturnsParseFailure()
202+
{
203+
var json = "not valid json {{{";
204+
205+
var response = ClaudeResponse.FromJson(json, TestDuration);
206+
207+
response.Success.Should().BeFalse();
208+
response.ErrorMessage.Should().Contain("Failed to parse");
209+
response.RawOutput.Should().Be(json);
210+
response.Duration.Should().Be(TestDuration);
211+
}
212+
213+
[Fact]
214+
public void FromJson_MissingUsage_DefaultsToZeroTokens()
215+
{
216+
var json = """{"result": "minimal"}""";
217+
218+
var response = ClaudeResponse.FromJson(json, TestDuration);
219+
220+
response.Success.Should().BeTrue();
221+
response.InputTokens.Should().Be(0);
222+
response.OutputTokens.Should().Be(0);
223+
response.CostUsd.Should().BeNull();
224+
}
225+
226+
[Fact]
227+
public void FromJson_MissingResult_ReturnsNullResult()
228+
{
229+
var json = """{"usage": {"input_tokens": 10, "output_tokens": 5}}""";
230+
231+
var response = ClaudeResponse.FromJson(json, TestDuration);
232+
233+
response.Success.Should().BeTrue();
234+
response.Result.Should().BeNull();
235+
}
236+
237+
#endregion
238+
239+
#region Failure factory
240+
241+
[Fact]
242+
public void Failure_CreatesFailedResponse()
243+
{
244+
var response = ClaudeResponse.Failure("bad stuff", 1, TestDuration);
245+
246+
response.Success.Should().BeFalse();
247+
response.ErrorMessage.Should().Be("bad stuff");
248+
response.ExitCode.Should().Be(1);
249+
response.Duration.Should().Be(TestDuration);
250+
response.Result.Should().BeNull();
251+
response.RawOutput.Should().BeNull();
252+
}
253+
254+
#endregion
255+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net10.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<RootNamespace>CodeGenesis.Engine.Tests</RootNamespace>
8+
<IsPackable>false</IsPackable>
9+
<IsTestProject>true</IsTestProject>
10+
</PropertyGroup>
11+
12+
<ItemGroup>
13+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
14+
<PackageReference Include="xunit" Version="2.9.3" />
15+
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.2" />
16+
<PackageReference Include="NSubstitute" Version="5.3.0" />
17+
<PackageReference Include="FluentAssertions" Version="8.2.0" />
18+
</ItemGroup>
19+
20+
<ItemGroup>
21+
<ProjectReference Include="..\CodeGenesis.Engine\CodeGenesis.Engine.csproj" />
22+
</ItemGroup>
23+
24+
</Project>

0 commit comments

Comments
 (0)