Skip to content

Commit 0b1083f

Browse files
committed
code style and fixes
1 parent 7878d9c commit 0b1083f

File tree

14 files changed

+254
-121
lines changed

14 files changed

+254
-121
lines changed

.github/workflows/real-integration.yml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ env:
1515
jobs:
1616
codex-real-integration:
1717
name: Real Codex Integration (${{ matrix.os }})
18-
if: ${{ secrets.OPENAI_API_KEY != '' }}
1918
strategy:
2019
fail-fast: false
2120
matrix:
@@ -47,6 +46,4 @@ jobs:
4746
- name: Run real integration tests
4847
run: dotnet test --project CodexSharpSDK.Tests/CodexSharpSDK.Tests.csproj -c Release -- --treenode-filter "/*/*/*/RunAsync_WithRealCodexCli_*"
4948
env:
50-
CODEX_REAL_INTEGRATION: "1"
51-
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
5249
CODEX_TEST_MODEL: gpt-5.3-codex

AGENTS.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ If no new rule is detected -> do not update the file.
7070
- analyze: `dotnet build ManagedCode.CodexSharpSDK.slnx -c Release -warnaserror /p:TreatWarningsAsErrors=true`
7171
- coverage: `dotnet test --solution ManagedCode.CodexSharpSDK.slnx -c Release -- --coverage --coverage-output-format cobertura --coverage-output coverage.cobertura.xml`
7272
- aot-smoke: `dotnet publish tests/AotSmoke/ManagedCode.CodexSharpSDK.AotSmoke.csproj -c Release -r osx-arm64 /p:PublishAot=true`
73+
- For Codex CLI metadata checks (for example model list/default), always use the installed `codex` CLI directly first; do not use `cargo`/submodule helper binaries unless explicitly requested.
7374

7475
### Task Delivery (ALL TASKS)
7576

@@ -120,7 +121,8 @@ If no new rule is detected -> do not update the file.
120121
- Integration test sandboxes must be created under the repository `tests` tree (for example `tests/.sandbox/*`) for deterministic, inspectable paths; do not use `Path.GetTempPath()` for test sandbox directories.
121122
- For CLI process interaction tests, use the real installed `codex` CLI (no `FakeCodexProcessRunner` test doubles).
122123
- Treat `codex` CLI as a test prerequisite: ensure local/CI test setup installs `codex` before running CLI interaction tests; do not replace this with fakes.
123-
- Real Codex integration tests must work with existing Codex CLI login/session by default; `OPENAI_API_KEY` is optional and must not be a hard requirement.
124+
- Real Codex integration tests must rely on existing local Codex CLI login/session only; do not read or require `OPENAI_API_KEY` in test setup.
125+
- Do not use nullable `TryGetSettings()` + early `return` skip patterns in real integration tests; resolve required settings directly and fail fast with actionable errors when missing.
124126
- Do not bypass integration tests on Windows with unconditional early returns; keep tests cross-platform for supported Codex CLI environments.
125127
- Parser changes require tests in `ThreadEventParserTests` for supported and invalid payloads.
126128
- Parser performance tests must use representative mixed payloads across supported event/item types and assert parsed output shape; avoid single-payload stopwatch loops that do not validate branch coverage.
@@ -147,6 +149,9 @@ If no new rule is detected -> do not update the file.
147149
- Never use empty/silent `catch` blocks; every caught exception must be either logged with context or rethrown with context.
148150
- Never add fake fallback calls/mocks in production paths; unsupported runtime cases must fail explicitly with actionable errors.
149151
- No magic literals: extract constants/enums/config values.
152+
- 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.
153+
- Do not inline filesystem/path segment string literals in implementation logic; define named constants and reuse them.
154+
- Never override or silently mutate explicit user-provided Codex CLI settings (for example `web_search=disabled`); pass through user intent exactly.
150155
- Protocol and CLI string tokens are mandatory constants: never inline literals in parsing, mapping, or switch branches.
151156
- In SDK model records, never inline protocol type literals in constructors (`ThreadItem(..., "...")`, `ThreadEvent("...")`); always reference protocol constants.
152157
- Do not expose a public SDK type named `Thread`; use `CodexThread` to avoid .NET type-name conflicts.
@@ -156,6 +161,7 @@ If no new rule is detected -> do not update the file.
156161
- Never hardcode guessed Codex/OpenAI model names in tests, docs, or defaults; verify supported models and active default via Codex CLI first.
157162
- Before setting or changing any `Model` value, read available models and current default from the local `codex` CLI in the same environment/account and only then update code/tests/docs.
158163
- Model identifiers in code/tests must come from centralized constants or a shared resolver helper; do not inline model string literals repeatedly.
164+
- Keep C# SDK request options in parity with upstream Codex TypeScript SDK/CLI capabilities and verify against upstream source before changing option surface.
159165
- Image input API must support passing local image data as file path, `FileInfo`, and `Stream`.
160166
- Use `Microsoft.Extensions.Logging.ILogger` for SDK logging extension points; do not introduce custom logger interfaces or custom log-level enums.
161167
- In tests, prefer `Microsoft.Extensions.Logging.Abstractions.NullLogger` instead of custom fake logger implementations when log capture is not required.

CodexSharpSDK.Tests/Integration/CodexExecIntegrationTests.cs

Lines changed: 9 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,7 @@ public class CodexExecIntegrationTests
1313
[Test]
1414
public async Task RunAsync_UsesDefaultProcessRunner_EndToEnd()
1515
{
16-
var settings = RealCodexTestSupport.TryGetSettings();
17-
if (settings is null)
18-
{
19-
return;
20-
}
16+
var settings = RealCodexTestSupport.GetRequiredSettings();
2117

2218
var exec = new CodexExec();
2319
using var cancellation = new CancellationTokenSource(TimeSpan.FromMinutes(2));
@@ -26,10 +22,10 @@ public async Task RunAsync_UsesDefaultProcessRunner_EndToEnd()
2622
{
2723
Input = FirstPrompt,
2824
Model = settings.Model,
29-
ModelReasoningEffort = ModelReasoningEffort.Minimal,
25+
ModelReasoningEffort = ModelReasoningEffort.Medium,
26+
WebSearchMode = WebSearchMode.Disabled,
3027
SandboxMode = SandboxMode.WorkspaceWrite,
3128
NetworkAccessEnabled = true,
32-
ApiKey = settings.ApiKey,
3329
CancellationToken = cancellation.Token,
3430
}));
3531

@@ -40,19 +36,16 @@ public async Task RunAsync_UsesDefaultProcessRunner_EndToEnd()
4036
[Test]
4137
public async Task RunAsync_SecondCallPassesResumeArgument_EndToEnd()
4238
{
43-
var settings = RealCodexTestSupport.TryGetSettings();
44-
if (settings is null)
45-
{
46-
return;
47-
}
39+
var settings = RealCodexTestSupport.GetRequiredSettings();
4840

49-
using var client = RealCodexTestSupport.CreateClient(settings);
41+
using var client = RealCodexTestSupport.CreateClient();
5042
using var cancellation = new CancellationTokenSource(TimeSpan.FromMinutes(3));
5143

5244
var thread = client.StartThread(new ThreadOptions
5345
{
5446
Model = settings.Model,
55-
ModelReasoningEffort = ModelReasoningEffort.Minimal,
47+
ModelReasoningEffort = ModelReasoningEffort.Medium,
48+
WebSearchMode = WebSearchMode.Disabled,
5649
SandboxMode = SandboxMode.WorkspaceWrite,
5750
NetworkAccessEnabled = true,
5851
});
@@ -76,11 +69,7 @@ public async Task RunAsync_SecondCallPassesResumeArgument_EndToEnd()
7669
[Test]
7770
public async Task RunAsync_PropagatesNonZeroExitCode_EndToEnd()
7871
{
79-
var settings = RealCodexTestSupport.TryGetSettings();
80-
if (settings is null)
81-
{
82-
return;
83-
}
72+
var settings = RealCodexTestSupport.GetRequiredSettings();
8473

8574
var exec = new CodexExec();
8675
using var cancellation = new CancellationTokenSource(TimeSpan.FromMinutes(2));
@@ -89,9 +78,9 @@ public async Task RunAsync_PropagatesNonZeroExitCode_EndToEnd()
8978
{
9079
Input = FirstPrompt,
9180
Model = InvalidModel,
81+
WebSearchMode = WebSearchMode.Disabled,
9282
SandboxMode = SandboxMode.WorkspaceWrite,
9383
NetworkAccessEnabled = true,
94-
ApiKey = settings.ApiKey,
9584
CancellationToken = cancellation.Token,
9685
}));
9786

CodexSharpSDK.Tests/Integration/RealCodexIntegrationTests.cs

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,9 @@ public class RealCodexIntegrationTests
99
[Test]
1010
public async Task RunAsync_WithRealCodexCli_ReturnsStructuredOutput()
1111
{
12-
var settings = RealCodexTestSupport.TryGetSettings();
13-
if (settings is null)
14-
{
15-
return;
16-
}
12+
var settings = RealCodexTestSupport.GetRequiredSettings();
1713

18-
using var client = RealCodexTestSupport.CreateClient(settings);
14+
using var client = RealCodexTestSupport.CreateClient();
1915
var thread = StartRealIntegrationThread(client, settings.Model);
2016

2117
using var cancellation = new CancellationTokenSource(TimeSpan.FromMinutes(2));
@@ -37,13 +33,9 @@ public async Task RunAsync_WithRealCodexCli_ReturnsStructuredOutput()
3733
[Test]
3834
public async Task RunStreamedAsync_WithRealCodexCli_YieldsCompletedTurnEvent()
3935
{
40-
var settings = RealCodexTestSupport.TryGetSettings();
41-
if (settings is null)
42-
{
43-
return;
44-
}
36+
var settings = RealCodexTestSupport.GetRequiredSettings();
4537

46-
using var client = RealCodexTestSupport.CreateClient(settings);
38+
using var client = RealCodexTestSupport.CreateClient();
4739
var thread = StartRealIntegrationThread(client, settings.Model);
4840
using var cancellation = new CancellationTokenSource(TimeSpan.FromMinutes(2));
4941

@@ -71,13 +63,9 @@ public async Task RunStreamedAsync_WithRealCodexCli_YieldsCompletedTurnEvent()
7163
[Test]
7264
public async Task RunAsync_WithRealCodexCli_SecondTurnKeepsThreadId()
7365
{
74-
var settings = RealCodexTestSupport.TryGetSettings();
75-
if (settings is null)
76-
{
77-
return;
78-
}
66+
var settings = RealCodexTestSupport.GetRequiredSettings();
7967

80-
using var client = RealCodexTestSupport.CreateClient(settings);
68+
using var client = RealCodexTestSupport.CreateClient();
8169
var thread = StartRealIntegrationThread(client, settings.Model);
8270
using var cancellation = new CancellationTokenSource(TimeSpan.FromMinutes(3));
8371

@@ -114,7 +102,8 @@ private static CodexThread StartRealIntegrationThread(CodexClient client, string
114102
return client.StartThread(new ThreadOptions
115103
{
116104
Model = model,
117-
ModelReasoningEffort = ModelReasoningEffort.Minimal,
105+
ModelReasoningEffort = ModelReasoningEffort.Medium,
106+
WebSearchMode = WebSearchMode.Disabled,
118107
SandboxMode = SandboxMode.WorkspaceWrite,
119108
NetworkAccessEnabled = true,
120109
});

CodexSharpSDK.Tests/Shared/IntegrationOutputModels.cs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Text.Json;
2+
using System.Text.Json.Serialization;
23
using ManagedCode.CodexSharpSDK.Models;
34

45
namespace ManagedCode.CodexSharpSDK.Tests.Shared;
@@ -31,8 +32,22 @@ public static T Deserialize<T>(string payload)
3132
{
3233
ArgumentException.ThrowIfNullOrWhiteSpace(payload);
3334

34-
var model = JsonSerializer.Deserialize<T>(payload) ?? throw new InvalidOperationException($"Failed to deserialize integration payload to {typeof(T).Name}.");
35+
if (typeof(T) == typeof(StatusResponse))
36+
{
37+
return (T)(object)(JsonSerializer.Deserialize(payload, IntegrationOutputJsonContext.Default.StatusResponse)
38+
?? throw new InvalidOperationException("Failed to deserialize integration payload to StatusResponse."));
39+
}
3540

36-
return model;
41+
if (typeof(T) == typeof(RepositorySummaryResponse))
42+
{
43+
return (T)(object)(JsonSerializer.Deserialize(payload, IntegrationOutputJsonContext.Default.RepositorySummaryResponse)
44+
?? throw new InvalidOperationException("Failed to deserialize integration payload to RepositorySummaryResponse."));
45+
}
46+
47+
throw new NotSupportedException($"IntegrationOutputDeserializer does not support type {typeof(T).Name}.");
3748
}
3849
}
50+
51+
[JsonSerializable(typeof(StatusResponse))]
52+
[JsonSerializable(typeof(RepositorySummaryResponse))]
53+
internal sealed partial class IntegrationOutputJsonContext : JsonSerializerContext;
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
using ManagedCode.CodexSharpSDK.Client;
2+
using ManagedCode.CodexSharpSDK.Configuration;
3+
using ManagedCode.CodexSharpSDK.Internal;
4+
5+
namespace ManagedCode.CodexSharpSDK.Tests.Shared;
6+
7+
internal static class RealCodexTestSupport
8+
{
9+
private const string ModelEnvVar = "CODEX_TEST_MODEL";
10+
11+
public static RealCodexTestSettings GetRequiredSettings()
12+
{
13+
if (!IsCodexAvailable())
14+
{
15+
throw new InvalidOperationException(
16+
"Real Codex tests require the codex CLI. Install it first and ensure it is available in PATH.");
17+
}
18+
19+
return new RealCodexTestSettings(ResolveModel());
20+
}
21+
22+
public static CodexClient CreateClient()
23+
{
24+
return new CodexClient(new CodexOptions());
25+
}
26+
27+
private static string ResolveModel()
28+
{
29+
var fromEnvironment = Environment.GetEnvironmentVariable(ModelEnvVar);
30+
if (!string.IsNullOrWhiteSpace(fromEnvironment))
31+
{
32+
return fromEnvironment;
33+
}
34+
35+
var fromConfig = TryReadModelFromCodexConfig();
36+
if (!string.IsNullOrWhiteSpace(fromConfig))
37+
{
38+
return fromConfig;
39+
}
40+
41+
throw new InvalidOperationException(
42+
$"Real Codex tests require a model. Set {ModelEnvVar} or define model in '~/.codex/config.toml'.");
43+
}
44+
45+
private static string? TryReadModelFromCodexConfig()
46+
{
47+
var configPath = GetCodexConfigPath();
48+
if (configPath is null || !File.Exists(configPath))
49+
{
50+
return null;
51+
}
52+
53+
try
54+
{
55+
foreach (var line in File.ReadLines(configPath))
56+
{
57+
var trimmed = line.Trim();
58+
if (!trimmed.StartsWith("model", StringComparison.Ordinal))
59+
{
60+
continue;
61+
}
62+
63+
var separatorIndex = trimmed.IndexOf('=');
64+
if (separatorIndex <= 0)
65+
{
66+
continue;
67+
}
68+
69+
var key = trimmed[..separatorIndex].Trim();
70+
if (!string.Equals(key, "model", StringComparison.Ordinal))
71+
{
72+
continue;
73+
}
74+
75+
var rawValue = trimmed[(separatorIndex + 1)..].Trim();
76+
if (rawValue.Length >= 2
77+
&& rawValue.StartsWith('"')
78+
&& rawValue.EndsWith('"'))
79+
{
80+
rawValue = rawValue[1..^1];
81+
}
82+
83+
return string.IsNullOrWhiteSpace(rawValue)
84+
? null
85+
: rawValue;
86+
}
87+
}
88+
catch (IOException exception)
89+
{
90+
throw new InvalidOperationException($"Failed to read Codex config at '{configPath}'.", exception);
91+
}
92+
catch (UnauthorizedAccessException exception)
93+
{
94+
throw new InvalidOperationException($"Failed to read Codex config at '{configPath}'.", exception);
95+
}
96+
97+
return null;
98+
}
99+
100+
private static string? GetCodexConfigPath()
101+
{
102+
var homeDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
103+
if (string.IsNullOrWhiteSpace(homeDirectory))
104+
{
105+
return null;
106+
}
107+
108+
return Path.Combine(homeDirectory, ".codex", "config.toml");
109+
}
110+
111+
private static bool IsCodexAvailable()
112+
{
113+
var resolvedPath = CodexCliLocator.FindCodexPath(null);
114+
if (Path.IsPathRooted(resolvedPath))
115+
{
116+
return File.Exists(resolvedPath);
117+
}
118+
119+
return CodexCliLocator.TryResolvePathExecutable(
120+
Environment.GetEnvironmentVariable("PATH"),
121+
OperatingSystem.IsWindows(),
122+
out _);
123+
}
124+
}
125+
126+
internal sealed record RealCodexTestSettings(string Model);

CodexSharpSDK.Tests/Unit/CodexClientTests.cs

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -231,19 +231,16 @@ public async Task Dispose_CanBeCalledConcurrently()
231231
[Test]
232232
public async Task ResumeThread_WithThreadOptions_RunsWithRealCodexCli()
233233
{
234-
var settings = RealCodexTestSupport.TryGetSettings();
235-
if (settings is null)
236-
{
237-
return;
238-
}
234+
var settings = RealCodexTestSupport.GetRequiredSettings();
239235

240-
using var client = RealCodexTestSupport.CreateClient(settings);
236+
using var client = RealCodexTestSupport.CreateClient();
241237
using var cancellation = new CancellationTokenSource(TimeSpan.FromMinutes(3));
242238

243239
var startedThread = client.StartThread(new ThreadOptions
244240
{
245241
Model = settings.Model,
246-
ModelReasoningEffort = ModelReasoningEffort.Minimal,
242+
ModelReasoningEffort = ModelReasoningEffort.Medium,
243+
WebSearchMode = WebSearchMode.Disabled,
247244
SandboxMode = SandboxMode.WorkspaceWrite,
248245
NetworkAccessEnabled = true,
249246
});
@@ -259,7 +256,8 @@ public async Task ResumeThread_WithThreadOptions_RunsWithRealCodexCli()
259256
var resumedThread = client.ResumeThread(threadId!, new ThreadOptions
260257
{
261258
Model = settings.Model,
262-
ModelReasoningEffort = ModelReasoningEffort.Minimal,
259+
ModelReasoningEffort = ModelReasoningEffort.Medium,
260+
WebSearchMode = WebSearchMode.Disabled,
263261
SandboxMode = SandboxMode.WorkspaceWrite,
264262
NetworkAccessEnabled = true,
265263
});

0 commit comments

Comments
 (0)