Skip to content

Commit 7878d9c

Browse files
committed
code
1 parent c85d49e commit 7878d9c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+680
-516
lines changed

.github/workflows/real-integration.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ jobs:
4545
run: dotnet restore ManagedCode.CodexSharpSDK.slnx
4646

4747
- name: Run real integration tests
48-
run: dotnet test --project tests/CodexSharpSDK.Tests.csproj -c Release -- --treenode-filter "/*/*/RealCodexIntegrationTests/*"
48+
run: dotnet test --project CodexSharpSDK.Tests/CodexSharpSDK.Tests.csproj -c Release -- --treenode-filter "/*/*/*/RunAsync_WithRealCodexCli_*"
4949
env:
5050
CODEX_REAL_INTEGRATION: "1"
5151
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}

AGENTS.md

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,13 @@ If no new rule is detected -> do not update the file.
139139
- Always build with `-warnaserror` so warnings fail the build.
140140
- Prefer idiomatic, readable C# with clear naming and straightforward control flow so code can be understood quickly during review and maintenance.
141141
- Use synchronization primitives only when there is a proven shared-state invariant; prefer simpler designs over ad-hoc locking for maintainable production code.
142+
- Keep a clear, domain-oriented folder structure (for example `Models`, `Client`, `Execution`, `Internal`) so project layout remains predictable and easy to navigate.
143+
- Never use sync-over-async bridges like `.GetAwaiter().GetResult()` in SDK code; keep disposal and lifecycle APIs non-blocking and explicit.
144+
- Do not implement both `IDisposable` and `IAsyncDisposable` on the same SDK type unless the type truly owns asynchronous resources that require async-only cleanup.
145+
- Never use `#pragma warning disable`; fix the underlying warning cause instead.
146+
- Keep short constructor initializers on the same line as the signature (for example `Ctor(...) : base(...)`), not on a separate line.
147+
- Never use empty/silent `catch` blocks; every caught exception must be either logged with context or rethrown with context.
148+
- Never add fake fallback calls/mocks in production paths; unsupported runtime cases must fail explicitly with actionable errors.
142149
- No magic literals: extract constants/enums/config values.
143150
- Protocol and CLI string tokens are mandatory constants: never inline literals in parsing, mapping, or switch branches.
144151
- In SDK model records, never inline protocol type literals in constructors (`ThreadItem(..., "...")`, `ThreadEvent("...")`); always reference protocol constants.
@@ -153,8 +160,9 @@ If no new rule is detected -> do not update the file.
153160
- Use `Microsoft.Extensions.Logging.ILogger` for SDK logging extension points; do not introduce custom logger interfaces or custom log-level enums.
154161
- In tests, prefer `Microsoft.Extensions.Logging.Abstractions.NullLogger` instead of custom fake logger implementations when log capture is not required.
155162
- Default to AOT/trimming-safe patterns (explicit JSON handling, avoid reflection-heavy designs).
156-
- Avoid ambiguous option names like `*Override` for primary settings; prefer explicit names (for example executable path / working directory) and keep compatibility aliases only when necessary.
157-
- README first examples must be beginner-friendly: avoid advanced/optional knobs (for example `CodexPathOverride`) in the very first snippet.
163+
- Avoid ambiguous option names like `*Override` for primary settings; prefer explicit names (for example executable path / working directory).
164+
- For this project, remove legacy/compatibility shims immediately (including `[Obsolete]` bridges and duplicate old properties); keep only the current API surface.
165+
- README first examples must be beginner-friendly: avoid advanced/optional knobs (for example `CodexExecutablePath`) in the very first snippet.
158166
- When a README snippet shows model tuning, include `ModelReasoningEffort` together with `Model`.
159167
- Public examples should build output schemas with typed `StructuredOutputSchema` models and map responses to typed DTOs for readability and maintainability.
160168
- Do not keep or add `JsonSchema` helper abstractions in SDK API/tests; use typed request/response DTO models instead of schema-builder utilities.
@@ -201,6 +209,13 @@ If no new rule is detected -> do not update the file.
201209
- Hidden assumptions in CI/release pipelines
202210
- Unreadable, non-idiomatic C# that looks chaotic and hard to reason about
203211
- Unjustified `lock`/thread-synchronization complexity that obscures intent and increases maintenance risk
212+
- Flat, mixed file layouts where model, client, execution, and utility types are interleaved without folder boundaries
213+
- Blocking sync-over-async patterns (`GetAwaiter().GetResult()`) in production SDK code
214+
- Legacy compatibility layers in a new project (`Obsolete` bridges, duplicate old option names, migration shims)
215+
- Suppressing warnings with `#pragma warning disable` instead of fixing root causes
216+
- Constructor initializer formatting split onto a separate line (`)` then next line `: base(...)`) in simple cases
217+
- Silent exception swallowing (`catch {}` or catch-without logging/context) that hides cleanup/runtime failures
218+
- Fake fallback behavior that masks real runtime/CLI integration issues
204219
- Template placeholders left in production repository docs
205220
- Raw nested `JsonObject`/`JsonArray` schema literals in user-facing examples.
206221
- Public sample projects in this repository; prefer tests (including AOT tests) instead.

CodexSharpSDK.Tests/CodexSharpSDK.Tests.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,6 @@
2424
</ItemGroup>
2525

2626
<ItemGroup>
27-
<ProjectReference Include="../src/CodexSharpSDK.csproj" />
27+
<ProjectReference Include="../CodexSharpSDK/CodexSharpSDK.csproj" />
2828
</ItemGroup>
2929
</Project>
Lines changed: 75 additions & 157 deletions
Original file line numberDiff line numberDiff line change
@@ -1,204 +1,122 @@
1-
namespace ManagedCode.CodexSharpSDK.Tests;
1+
using ManagedCode.CodexSharpSDK.Client;
2+
using ManagedCode.CodexSharpSDK.Execution;
3+
using ManagedCode.CodexSharpSDK.Tests.Shared;
4+
5+
namespace ManagedCode.CodexSharpSDK.Tests.Integration;
26

37
public class CodexExecIntegrationTests
48
{
5-
private const string SandboxRootDirectoryName = ".sandbox";
6-
private const string SandboxDirectoryPrefix = "codexsharp-integration-";
9+
private const string FirstPrompt = "Reply with short plain text: first.";
10+
private const string SecondPrompt = "Reply with short plain text: second.";
11+
private const string InvalidModel = "__codexsharp_invalid_model__";
712

813
[Test]
914
public async Task RunAsync_UsesDefaultProcessRunner_EndToEnd()
1015
{
11-
if (OperatingSystem.IsWindows())
16+
var settings = RealCodexTestSupport.TryGetSettings();
17+
if (settings is null)
1218
{
1319
return;
1420
}
1521

16-
var sandboxDirectory = CreateSandboxDirectory();
17-
try
18-
{
19-
var argsLog = Path.Combine(sandboxDirectory, "args.log");
20-
var inputLog = Path.Combine(sandboxDirectory, "input.log");
21-
var executablePath = Path.Combine(sandboxDirectory, "fake-codex.sh");
22-
23-
WriteExecutableScript(executablePath, BuildSuccessScript(argsLog, inputLog, "thread_it_1", "integration_ok"));
24-
25-
await using var client = new CodexClient(new CodexOptions
26-
{
27-
CodexPathOverride = executablePath,
28-
});
29-
30-
var thread = client.StartThread(new ThreadOptions
31-
{
32-
Model = "gpt-5.3-codex",
33-
SandboxMode = SandboxMode.WorkspaceWrite,
34-
});
35-
36-
var result = await thread.RunAsync("hello from integration");
37-
38-
await Assert.That(thread.Id).IsEqualTo("thread_it_1");
39-
await Assert.That(result.FinalResponse).IsEqualTo("integration_ok");
40-
await Assert.That(result.Usage).IsNotNull();
41-
42-
var args = await File.ReadAllLinesAsync(argsLog);
43-
await Assert.That(args).Contains("exec");
44-
await Assert.That(args).Contains("--experimental-json");
45-
await Assert.That(args).Contains("--model");
46-
await Assert.That(args).Contains("gpt-5.3-codex");
47-
await Assert.That(args).Contains("--sandbox");
48-
await Assert.That(args).Contains("workspace-write");
49-
50-
var input = await File.ReadAllTextAsync(inputLog);
51-
await Assert.That(input).IsEqualTo("hello from integration");
52-
}
53-
finally
22+
var exec = new CodexExec();
23+
using var cancellation = new CancellationTokenSource(TimeSpan.FromMinutes(2));
24+
25+
var lines = await DrainToListAsync(exec.RunAsync(new CodexExecArgs
5426
{
55-
CleanupSandboxDirectory(sandboxDirectory);
56-
}
27+
Input = FirstPrompt,
28+
Model = settings.Model,
29+
ModelReasoningEffort = ModelReasoningEffort.Minimal,
30+
SandboxMode = SandboxMode.WorkspaceWrite,
31+
NetworkAccessEnabled = true,
32+
ApiKey = settings.ApiKey,
33+
CancellationToken = cancellation.Token,
34+
}));
35+
36+
await Assert.That(lines.Any(line => line.Contains("\"type\":\"thread.started\"", StringComparison.Ordinal))).IsTrue();
37+
await Assert.That(lines.Any(line => line.Contains("\"type\":\"turn.completed\"", StringComparison.Ordinal))).IsTrue();
5738
}
5839

5940
[Test]
6041
public async Task RunAsync_SecondCallPassesResumeArgument_EndToEnd()
6142
{
62-
if (OperatingSystem.IsWindows())
43+
var settings = RealCodexTestSupport.TryGetSettings();
44+
if (settings is null)
6345
{
6446
return;
6547
}
6648

67-
var sandboxDirectory = CreateSandboxDirectory();
68-
try
69-
{
70-
var argsLog = Path.Combine(sandboxDirectory, "args.log");
71-
var inputLog = Path.Combine(sandboxDirectory, "input.log");
72-
var executablePath = Path.Combine(sandboxDirectory, "fake-codex.sh");
73-
74-
WriteExecutableScript(executablePath, BuildSuccessScript(argsLog, inputLog, "thread_it_2", "ok"));
75-
76-
await using var client = new CodexClient(new CodexOptions
77-
{
78-
CodexPathOverride = executablePath,
79-
});
80-
81-
var thread = client.StartThread();
49+
using var client = RealCodexTestSupport.CreateClient(settings);
50+
using var cancellation = new CancellationTokenSource(TimeSpan.FromMinutes(3));
8251

83-
await thread.RunAsync("first");
84-
await thread.RunAsync("second");
85-
86-
var args = await File.ReadAllLinesAsync(argsLog);
87-
var resumeIndex = Array.IndexOf(args, "resume");
88-
89-
await Assert.That(resumeIndex).IsGreaterThan(-1);
90-
await Assert.That(args[resumeIndex + 1]).IsEqualTo("thread_it_2");
91-
}
92-
finally
52+
var thread = client.StartThread(new ThreadOptions
9353
{
94-
CleanupSandboxDirectory(sandboxDirectory);
95-
}
54+
Model = settings.Model,
55+
ModelReasoningEffort = ModelReasoningEffort.Minimal,
56+
SandboxMode = SandboxMode.WorkspaceWrite,
57+
NetworkAccessEnabled = true,
58+
});
59+
60+
var firstResult = await thread.RunAsync(
61+
FirstPrompt,
62+
new TurnOptions { CancellationToken = cancellation.Token });
63+
64+
var threadId = thread.Id;
65+
await Assert.That(threadId).IsNotNull();
66+
await Assert.That(firstResult.Usage).IsNotNull();
67+
68+
var secondResult = await thread.RunAsync(
69+
SecondPrompt,
70+
new TurnOptions { CancellationToken = cancellation.Token });
71+
72+
await Assert.That(secondResult.Usage).IsNotNull();
73+
await Assert.That(thread.Id).IsEqualTo(threadId);
9674
}
9775

9876
[Test]
9977
public async Task RunAsync_PropagatesNonZeroExitCode_EndToEnd()
10078
{
101-
if (OperatingSystem.IsWindows())
79+
var settings = RealCodexTestSupport.TryGetSettings();
80+
if (settings is null)
10281
{
10382
return;
10483
}
10584

106-
var sandboxDirectory = CreateSandboxDirectory();
107-
try
108-
{
109-
var executablePath = Path.Combine(sandboxDirectory, "fake-codex.sh");
110-
WriteExecutableScript(executablePath, BuildFailureScript());
85+
var exec = new CodexExec();
86+
using var cancellation = new CancellationTokenSource(TimeSpan.FromMinutes(2));
11187

112-
await using var client = new CodexClient(new CodexOptions
113-
{
114-
CodexPathOverride = executablePath,
115-
});
116-
117-
var thread = client.StartThread();
118-
var action = async () => await thread.RunAsync("trigger failure");
119-
120-
var exception = await Assert.That(action).ThrowsException();
121-
await Assert.That(exception).IsTypeOf<InvalidOperationException>();
122-
await Assert.That(exception!.Message).Contains("exited with code 9");
123-
}
124-
finally
88+
var action = async () => await DrainAsync(exec.RunAsync(new CodexExecArgs
12589
{
126-
CleanupSandboxDirectory(sandboxDirectory);
127-
}
90+
Input = FirstPrompt,
91+
Model = InvalidModel,
92+
SandboxMode = SandboxMode.WorkspaceWrite,
93+
NetworkAccessEnabled = true,
94+
ApiKey = settings.ApiKey,
95+
CancellationToken = cancellation.Token,
96+
}));
97+
98+
var exception = await Assert.That(action).ThrowsException();
99+
await Assert.That(exception).IsTypeOf<InvalidOperationException>();
100+
await Assert.That(exception!.Message).Contains("exited with code");
128101
}
129102

130-
private static string CreateSandboxDirectory()
103+
private static async Task DrainAsync(IAsyncEnumerable<string> lines)
131104
{
132-
var testsDirectory = Environment.CurrentDirectory;
133-
var sandboxRootDirectory = Path.Combine(testsDirectory, SandboxRootDirectoryName);
134-
Directory.CreateDirectory(sandboxRootDirectory);
135-
136-
var directory = Path.Combine(sandboxRootDirectory, $"{SandboxDirectoryPrefix}{Guid.NewGuid():N}");
137-
Directory.CreateDirectory(directory);
138-
return directory;
139-
}
140-
141-
private static void CleanupSandboxDirectory(string directory)
142-
{
143-
try
105+
await foreach (var _ in lines)
144106
{
145-
if (Directory.Exists(directory))
146-
{
147-
Directory.Delete(directory, recursive: true);
148-
}
107+
// Intentionally empty.
149108
}
150-
catch
151-
{
152-
// Suppress cleanup errors.
153-
}
154-
}
155-
156-
private static string BuildSuccessScript(string argsLog, string inputLog, string threadId, string response)
157-
{
158-
var escapedThreadId = EscapeJsonString(threadId);
159-
var escapedResponse = EscapeJsonString(response);
160-
161-
return string.Join('\n',
162-
[
163-
"#!/usr/bin/env bash",
164-
"set -euo pipefail",
165-
$"args_log={ToBashLiteral(argsLog)}",
166-
$"input_log={ToBashLiteral(inputLog)}",
167-
"printf '%s\\n' \"$@\" > \"$args_log\"",
168-
"cat > \"$input_log\"",
169-
$"echo '{{\"type\":\"thread.started\",\"thread_id\":\"{escapedThreadId}\"}}'",
170-
$"echo '{{\"type\":\"item.completed\",\"item\":{{\"id\":\"item_1\",\"type\":\"agent_message\",\"text\":\"{escapedResponse}\"}}}}'",
171-
"echo '{\"type\":\"turn.completed\",\"usage\":{\"input_tokens\":2,\"cached_input_tokens\":0,\"output_tokens\":3}}'",
172-
]) + "\n";
173109
}
174110

175-
private static string BuildFailureScript()
111+
private static async Task<List<string>> DrainToListAsync(IAsyncEnumerable<string> lines)
176112
{
177-
return """
178-
#!/usr/bin/env bash
179-
set -euo pipefail
180-
cat > /dev/null
181-
echo "forced integration failure" >&2
182-
exit 9
183-
""";
184-
}
113+
var result = new List<string>();
185114

186-
private static string ToBashLiteral(string value)
187-
{
188-
return $"'{value.Replace("'", "'\"'\"'")}'";
189-
}
190-
191-
private static string EscapeJsonString(string value)
192-
{
193-
return value.Replace("\\", "\\\\").Replace("\"", "\\\"");
194-
}
195-
196-
private static void WriteExecutableScript(string path, string scriptContent)
197-
{
198-
File.WriteAllText(path, scriptContent);
199-
if (!OperatingSystem.IsWindows())
115+
await foreach (var line in lines)
200116
{
201-
File.SetUnixFileMode(path, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute);
117+
result.Add(line);
202118
}
119+
120+
return result;
203121
}
204122
}

0 commit comments

Comments
 (0)