@dynokostya/just-works 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/.claude/agents/csharp-code-writer.md +32 -0
  2. package/.claude/agents/diagrammer.md +49 -0
  3. package/.claude/agents/frontend-code-writer.md +36 -0
  4. package/.claude/agents/prompt-writer.md +38 -0
  5. package/.claude/agents/python-code-writer.md +32 -0
  6. package/.claude/agents/swift-code-writer.md +32 -0
  7. package/.claude/agents/typescript-code-writer.md +32 -0
  8. package/.claude/commands/git-sync.md +96 -0
  9. package/.claude/commands/project-docs.md +287 -0
  10. package/.claude/settings.json +112 -0
  11. package/.claude/settings.json.default +15 -0
  12. package/.claude/skills/csharp-coding/SKILL.md +368 -0
  13. package/.claude/skills/ddd-architecture-python/SKILL.md +288 -0
  14. package/.claude/skills/feature-driven-architecture-python/SKILL.md +302 -0
  15. package/.claude/skills/gemini-3-prompting/SKILL.md +483 -0
  16. package/.claude/skills/gpt-5-2-prompting/SKILL.md +295 -0
  17. package/.claude/skills/opus-4-6-prompting/SKILL.md +315 -0
  18. package/.claude/skills/plantuml-diagramming/SKILL.md +758 -0
  19. package/.claude/skills/python-coding/SKILL.md +293 -0
  20. package/.claude/skills/react-coding/SKILL.md +264 -0
  21. package/.claude/skills/rest-api/SKILL.md +421 -0
  22. package/.claude/skills/shadcn-ui-coding/SKILL.md +454 -0
  23. package/.claude/skills/swift-coding/SKILL.md +401 -0
  24. package/.claude/skills/tailwind-css-coding/SKILL.md +268 -0
  25. package/.claude/skills/typescript-coding/SKILL.md +464 -0
  26. package/.claude/statusline-command.sh +34 -0
  27. package/.codex/prompts/plan-reviewer.md +162 -0
  28. package/.codex/prompts/project-docs.md +287 -0
  29. package/.codex/skills/ddd-architecture-python/SKILL.md +288 -0
  30. package/.codex/skills/feature-driven-architecture-python/SKILL.md +302 -0
  31. package/.codex/skills/gemini-3-prompting/SKILL.md +483 -0
  32. package/.codex/skills/gpt-5-2-prompting/SKILL.md +295 -0
  33. package/.codex/skills/opus-4-6-prompting/SKILL.md +315 -0
  34. package/.codex/skills/plantuml-diagramming/SKILL.md +758 -0
  35. package/.codex/skills/python-coding/SKILL.md +293 -0
  36. package/.codex/skills/react-coding/SKILL.md +264 -0
  37. package/.codex/skills/rest-api/SKILL.md +421 -0
  38. package/.codex/skills/shadcn-ui-coding/SKILL.md +454 -0
  39. package/.codex/skills/tailwind-css-coding/SKILL.md +268 -0
  40. package/.codex/skills/typescript-coding/SKILL.md +464 -0
  41. package/AGENTS.md +57 -0
  42. package/CLAUDE.md +98 -0
  43. package/LICENSE +201 -0
  44. package/README.md +114 -0
  45. package/bin/cli.mjs +291 -0
  46. package/package.json +39 -0
@@ -0,0 +1,112 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(cd:*)",
5
+ "Bash(find:*)",
6
+ "Bash(ls:*)",
7
+ "Bash(grep:*)",
8
+ "Bash(tree:*)",
9
+ "Bash(wc:*)",
10
+ "Bash(file:*)",
11
+ "Bash(du:*)",
12
+ "Bash(which:*)",
13
+ "Bash(diff:*)",
14
+ "Bash(pwd:*)",
15
+ "Bash(head:*)",
16
+ "Bash(tail:*)",
17
+ "Bash(sort:*)",
18
+ "Bash(uniq:*)",
19
+ "Bash(echo:*)"
20
+ ],
21
+ "deny": [
22
+ "Read(**/.env)",
23
+ "Read(**/.env*)",
24
+ "Read(**/*.pem)",
25
+ "Read(**/*.key)",
26
+ "Read(**/*.p12)",
27
+ "Read(**/*.pfx)",
28
+ "Read(**/*.jks)",
29
+ "Read(**/.netrc)",
30
+ "Read(**/.pgpass)",
31
+ "Read(**/.aws/*)",
32
+ "Read(**/.gcp/*)",
33
+ "Read(**/service-account*.json)",
34
+ "Read(**/.azure/*)",
35
+ "Read(**/terraform.tfvars)",
36
+ "Read(**/*.tfstate)",
37
+ "Read(**/*.tfstate.*)",
38
+ "Read(**/.ssh/*)",
39
+ "Read(**/.gnupg/*)",
40
+ "Read(**/*.sqlite)",
41
+ "Read(**/*.db)",
42
+ "Read(**/.htpasswd)"
43
+ ],
44
+ "defaultMode": "bypassPermissions"
45
+ },
46
+ "model": "opus[1m]",
47
+ "enableAllProjectMcpServers": true,
48
+ "enabledMcpjsonServers": [
49
+ "Context7"
50
+ ],
51
+ "cleanupPeriodDays": 180,
52
+ "maxTerminalOutputChars": 150000,
53
+ "maxFileReadTokens": 30000,
54
+ "autoCompactPercentageOverride": 80,
55
+ "autoMemoryEnabled": false,
56
+ "skipDangerousModePermissionPrompt": true,
57
+ "plansDirectory": "./.claude/plans",
58
+ "outputStyle": "default",
59
+ "spinnerTipsEnabled": true,
60
+ "attribution": {
61
+ "commit": "",
62
+ "pr": ""
63
+ },
64
+ "env": {
65
+ "CLAUDE_CODE_SUBAGENT_MODEL": "opus",
66
+ "CLAUDE_CODE_EFFORT_LEVEL": "max",
67
+ "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1",
68
+ "DISABLE_TELEMETRY": "1",
69
+ "DISABLE_ERROR_REPORTING": "1",
70
+ "DISABLE_FEEDBACK_COMMAND": "1",
71
+ "CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY": "1",
72
+ "CLAUDE_CODE_DISABLE_FAST_MODE": "1",
73
+ "BASH_MAX_OUTPUT_LENGTH": "150000"
74
+ },
75
+ "hooks": {
76
+ "PreToolUse": [
77
+ {
78
+ "matcher": "Read",
79
+ "hooks": [
80
+ {
81
+ "type": "command",
82
+ "command": "FILE=$(echo '$CLAUDE_TOOL_INPUT' | jq -r '.file_path') && [ -f \"$FILE\" ] && LINES=$(wc -l < \"$FILE\") && [ \"$LINES\" -gt 500 ] && echo 'File is too large, do not read it fully. Use offset and limit parameters to read specific sections.' >&2 && exit 2 || exit 0"
83
+ }
84
+ ]
85
+ },
86
+ {
87
+ "matcher": "Bash",
88
+ "hooks": [
89
+ {
90
+ "type": "command",
91
+ "command": "/Users/dynokostya/.claude/hooks/rtk-rewrite.sh"
92
+ }
93
+ ]
94
+ }
95
+ ],
96
+ "Notification": [
97
+ {
98
+ "matcher": "*",
99
+ "hooks": [
100
+ {
101
+ "type": "command",
102
+ "command": "afplay /System/Library/Sounds/Glass.aiff"
103
+ }
104
+ ]
105
+ }
106
+ ]
107
+ },
108
+ "statusLine": {
109
+ "type": "command",
110
+ "command": "bash ~/.claude/statusline-command.sh"
111
+ }
112
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [],
4
+ "deny": []
5
+ },
6
+ "hooks": {},
7
+ "outputStyle": "default",
8
+ "spinnerTipsEnabled": true,
9
+ "plansDirectory": "./.claude/plans",
10
+ "statusLine": {
11
+ "type": "command",
12
+ "command": "bash ~/.claude/statusline-command.sh"
13
+ }
14
+ }
15
+
@@ -0,0 +1,368 @@
1
+ ---
2
+ name: csharp-coding
3
+ description: Apply when writing or editing C# (.cs) files. Behavioral corrections for error handling, resource management, async patterns, data modeling, type safety, nullability, security defaults, and common antipatterns. Project conventions always override these defaults.
4
+ ---
5
+
6
+ # C# Coding
7
+
8
+ Match the project's existing conventions. When uncertain, read 2-3 existing files to infer the local style. Check `.csproj` for target framework, C# language version, nullable settings, and analyzer config. These defaults apply only when the project has no established convention.
9
+
10
+ ## Never rules
11
+
12
+ These are unconditional. They prevent bugs and vulnerabilities regardless of project style.
13
+
14
+ - **Never `catch { }` or `catch (Exception) { }` without rethrow or logging.** Broad catches silently swallow bugs — a NullReferenceException from a typo looks the same as a transient network failure. Catch specific exception types. If you must catch broadly at a boundary, always log and rethrow or convert to a meaningful error.
15
+ - **Never `throw ex`** — use `throw` or `throw new XException("msg", ex)`. `throw ex` resets the stack trace, destroying the origin of the error. You'll spend hours debugging something the original stack trace would have shown instantly.
16
+ - **Never `DateTime.Now`** — use `DateTime.UtcNow` or `DateTimeOffset.UtcNow`. `DateTime.Now` produces local time that varies by server timezone and breaks across DST transitions. `DateTimeOffset` is preferred when the timezone context matters.
17
+ - **Never `async void`** — except for event handlers. `async void` methods cannot be awaited, their exceptions crash the process unobserved, and they break structured error handling. Use `async Task` instead.
18
+ - **Never `.Result`, `.Wait()`, or `.GetAwaiter().GetResult()` on tasks** — these block the calling thread and cause deadlocks in ASP.NET Core and UI contexts. Use `await` instead. If you're in a sync context that genuinely cannot be made async, use `Task.Run(() => AsyncMethod()).GetAwaiter().GetResult()` as a last resort with a comment explaining why.
19
+ - **Never `Random` for security** — `Random` is not cryptographically secure and is predictable. Use `RandomNumberGenerator.GetBytes()`, `RandomNumberGenerator.GetInt32()`, or `Convert.ToHexString(RandomNumberGenerator.GetBytes(n))` for tokens, keys, session IDs.
20
+ - **Never string interpolation or concatenation in SQL** — use parameterized queries only. `$"SELECT * FROM users WHERE id = {uid}"` is always a SQL injection vulnerability. Use `@param` placeholders with `SqlCommand.Parameters` or your ORM's parameterization.
21
+ - **Never `GC.Collect()`** — the GC is self-tuning. Forcing collection hurts performance in nearly all cases by promoting short-lived objects to higher generations. If you think you need it, you have a design problem to fix instead.
22
+ - **Never mutable static fields** — statics are shared across all threads. Mutable statics cause race conditions that are extremely hard to reproduce and debug. Use `IOptions<T>`, dependency injection, or `ConcurrentDictionary` if shared state is genuinely needed.
23
+ - **Never `+` string concatenation in loops** — use `StringBuilder`. Strings are immutable in C#, so each `+` allocates a new string and copies all previous content — O(n^2) at scale. At 10k iterations this turns milliseconds into seconds.
24
+ - **Never `dynamic` for typed data** — `dynamic` bypasses compile-time type checking, turns type errors into runtime exceptions, and kills IntelliSense. Use generics, interfaces, or pattern matching instead. Reserve `dynamic` for COM interop or truly dynamic scenarios.
25
+ - **Never `Thread.Sleep()` in async code** — use `await Task.Delay()`. `Thread.Sleep` blocks the thread pool thread, starving other async operations. In ASP.NET Core this can exhaust the thread pool under load.
26
+
27
+ ## Error handling
28
+
29
+ Use `throw` (not `throw ex`) when re-throwing to preserve the original stack trace. Wrap with inner exception when converting exception types at boundaries:
30
+
31
+ ```csharp
32
+ try
33
+ {
34
+ var result = await httpClient.GetAsync(url, cancellationToken);
35
+ result.EnsureSuccessStatusCode();
36
+ }
37
+ catch (HttpRequestException ex)
38
+ {
39
+ throw new ServiceUnavailableException($"Failed to reach {url}", ex);
40
+ }
41
+ ```
42
+
43
+ Use exception filters (`when`) to catch conditionally without unwinding the stack:
44
+
45
+ ```csharp
46
+ catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
47
+ {
48
+ return null;
49
+ }
50
+ catch (HttpRequestException ex) when (ex.StatusCode >= HttpStatusCode.InternalServerError)
51
+ {
52
+ logger.LogWarning(ex, "Transient server error from {Url}", url);
53
+ throw;
54
+ }
55
+ ```
56
+
57
+ Create custom exception types when callers need to distinguish failure modes:
58
+
59
+ ```csharp
60
+ public class AppException : Exception
61
+ {
62
+ public AppException(string message) : base(message) { }
63
+ public AppException(string message, Exception inner) : base(message, inner) { }
64
+ }
65
+
66
+ public class NotFoundException : AppException
67
+ {
68
+ public string Resource { get; }
69
+ public object Id { get; }
70
+
71
+ public NotFoundException(string resource, object id)
72
+ : base($"{resource} not found: {id}")
73
+ {
74
+ Resource = resource;
75
+ Id = id;
76
+ }
77
+ }
78
+ ```
79
+
80
+ ## Resource cleanup
81
+
82
+ Use `using` declarations for anything that implements `IDisposable` or `IAsyncDisposable`. Never instantiate `HttpClient`, database connections, streams, or similar without `using` or explicit `finally` cleanup.
83
+
84
+ ```csharp
85
+ // C# 8+ using declaration — disposed at end of scope
86
+ using var stream = File.OpenRead(path);
87
+ using var reader = new StreamReader(stream);
88
+ var content = await reader.ReadToEndAsync(cancellationToken);
89
+ ```
90
+
91
+ For async disposables:
92
+
93
+ ```csharp
94
+ await using var connection = new SqlConnection(connectionString);
95
+ await connection.OpenAsync(cancellationToken);
96
+ ```
97
+
98
+ For multiple resources or complex lifecycles, use explicit `try/finally`:
99
+
100
+ ```csharp
101
+ var connection = new SqlConnection(connectionString);
102
+ try
103
+ {
104
+ await connection.OpenAsync(cancellationToken);
105
+ // work
106
+ }
107
+ finally
108
+ {
109
+ await connection.DisposeAsync();
110
+ }
111
+ ```
112
+
113
+ When implementing `IDisposable`, follow the pattern only if you hold unmanaged resources or own other disposables. Don't implement `IDisposable` on classes that don't own anything disposable — it adds ceremony for no benefit.
114
+
115
+ ## Async patterns
116
+
117
+ **Always pass `CancellationToken` through the call chain.** Accept it as the last parameter and forward it to every async API. Omitting it means your code cannot be cancelled, leading to wasted resources and hung requests.
118
+
119
+ ```csharp
120
+ public async Task<User> GetUserAsync(int id, CancellationToken cancellationToken = default)
121
+ {
122
+ var response = await _httpClient.GetAsync($"/users/{id}", cancellationToken);
123
+ response.EnsureSuccessStatusCode();
124
+ return await response.Content.ReadFromJsonAsync<User>(cancellationToken: cancellationToken)
125
+ ?? throw new NotFoundException("User", id);
126
+ }
127
+ ```
128
+
129
+ **Prefer `Task` over `ValueTask`** unless profiling shows allocation pressure from a hot path that frequently completes synchronously. `ValueTask` has restrictions (can only be awaited once, no concurrent awaits) that make it error-prone as a default choice.
130
+
131
+ **Use `ConfigureAwait(false)` in library code** (NuGet packages, shared class libraries) to avoid deadlocks in non-ASP.NET Core consumers. In ASP.NET Core application code, it's unnecessary — the default `SynchronizationContext` is null.
132
+
133
+ **Use `Task.WhenAll` for independent concurrent work:**
134
+
135
+ ```csharp
136
+ var usersTask = GetUsersAsync(cancellationToken);
137
+ var ordersTask = GetOrdersAsync(cancellationToken);
138
+
139
+ await Task.WhenAll(usersTask, ordersTask);
140
+
141
+ var users = await usersTask;
142
+ var orders = await ordersTask;
143
+ ```
144
+
145
+ **Never use `async` on a method that just returns another task.** Remove the state machine overhead:
146
+
147
+ ```csharp
148
+ // Wrong — unnecessary async state machine
149
+ public async Task<User> GetUserAsync(int id, CancellationToken ct)
150
+ => await _repository.GetByIdAsync(id, ct);
151
+
152
+ // Correct — direct passthrough
153
+ public Task<User> GetUserAsync(int id, CancellationToken ct)
154
+ => _repository.GetByIdAsync(id, ct);
155
+ ```
156
+
157
+ Exception: keep `async`/`await` when you need `using`, `try/catch`, or when the method does work before/after the awaited call.
158
+
159
+ ## Nullability
160
+
161
+ Enable nullable reference types (`<Nullable>enable</Nullable>` in .csproj). Use the compiler's flow analysis — don't add redundant null checks where the type system already guarantees non-null.
162
+
163
+ **Use `??` for defaults and `?.` for optional chains:**
164
+
165
+ ```csharp
166
+ var name = user.DisplayName ?? user.Email ?? "Anonymous";
167
+ var city = user.Address?.City;
168
+ ```
169
+
170
+ **Use `??=` for lazy initialization:**
171
+
172
+ ```csharp
173
+ _cache ??= new Dictionary<string, object>();
174
+ ```
175
+
176
+ **Guard clause pattern for required non-null arguments:**
177
+
178
+ ```csharp
179
+ public UserService(IUserRepository repository, ILogger<UserService> logger)
180
+ {
181
+ _repository = repository ?? throw new ArgumentNullException(nameof(repository));
182
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
183
+ }
184
+ ```
185
+
186
+ With C# 11+ and nullable enabled, prefer `required` keyword or primary constructors over null checks for constructor injection when the DI container guarantees non-null.
187
+
188
+ ## Pattern matching
189
+
190
+ Use switch expressions for exhaustive branching. Clearer than if/elif chains for type checks, property destructuring, and value mapping.
191
+
192
+ ```csharp
193
+ var message = result switch
194
+ {
195
+ { IsSuccess: true, Value: var v } => $"Got {v}",
196
+ { Error: NotFoundError e } => $"{e.Resource} not found",
197
+ { Error: ValidationError e } => $"Invalid: {string.Join(", ", e.Errors)}",
198
+ _ => "Unknown error"
199
+ };
200
+ ```
201
+
202
+ Property patterns for conditional logic:
203
+
204
+ ```csharp
205
+ if (order is { Status: OrderStatus.Shipped, TrackingNumber: not null } shipped)
206
+ {
207
+ NotifyCustomer(shipped.TrackingNumber);
208
+ }
209
+ ```
210
+
211
+ Relational and logical patterns:
212
+
213
+ ```csharp
214
+ var tier = score switch
215
+ {
216
+ >= 90 => "Gold",
217
+ >= 70 => "Silver",
218
+ >= 50 => "Bronze",
219
+ _ => "None"
220
+ };
221
+ ```
222
+
223
+ ## Data modeling
224
+
225
+ | Use Case | Choice | Reason |
226
+ |----------|--------|--------|
227
+ | API request/response DTOs | record | Immutable, value equality, concise syntax |
228
+ | Configuration | record or POCO + IOptions<T> | Framework support, binding |
229
+ | Domain entities with identity | class | Reference equality, mutable state |
230
+ | Simple value objects | readonly record struct | Stack-allocated, no GC pressure, value semantics |
231
+
232
+ Records for DTOs:
233
+
234
+ ```csharp
235
+ public sealed record CreateUserRequest(string Email, string Name);
236
+
237
+ public sealed record UserResponse(int Id, string Email, string Name);
238
+ ```
239
+
240
+ Primary constructors (C# 12) for service classes:
241
+
242
+ ```csharp
243
+ public sealed class UserService(IUserRepository repository, ILogger<UserService> logger)
244
+ {
245
+ public async Task<User> GetByIdAsync(int id, CancellationToken ct)
246
+ {
247
+ logger.LogDebug("Fetching user {UserId}", id);
248
+ return await repository.GetByIdAsync(id, ct)
249
+ ?? throw new NotFoundException("User", id);
250
+ }
251
+ }
252
+ ```
253
+
254
+ Use `init` properties when you need object initializer syntax with immutability:
255
+
256
+ ```csharp
257
+ public sealed class PaginationOptions
258
+ {
259
+ public int Page { get; init; } = 1;
260
+ public int PageSize { get; init; } = 20;
261
+ }
262
+ ```
263
+
264
+ ## Dependency injection
265
+
266
+ Constructor injection. Dependencies are explicit and testable.
267
+
268
+ ```csharp
269
+ public sealed class OrderService(
270
+ IOrderRepository repository,
271
+ IPaymentClient payments,
272
+ ILogger<OrderService> logger)
273
+ {
274
+ // ...
275
+ }
276
+ ```
277
+
278
+ **Service lifetimes:**
279
+ - **Transient** — lightweight, stateless services. New instance per injection.
280
+ - **Scoped** — per-request state (DbContext, unit of work, caller context). One instance per HTTP request.
281
+ - **Singleton** — thread-safe shared state (caches, configuration, HTTP client factories). One instance for app lifetime.
282
+
283
+ Match the project's DI container (Microsoft.Extensions.DependencyInjection, Autofac, etc.). Don't mix container-specific APIs across the codebase.
284
+
285
+ **Register by interface, not concrete type:**
286
+
287
+ ```csharp
288
+ services.AddScoped<IUserRepository, UserRepository>();
289
+ services.AddSingleton<ICacheService, RedisCacheService>();
290
+ ```
291
+
292
+ ## Enums
293
+
294
+ Use enums for known fixed value sets. Don't use raw strings or magic numbers.
295
+
296
+ ```csharp
297
+ public enum OrderStatus
298
+ {
299
+ Pending,
300
+ Confirmed,
301
+ Shipped,
302
+ Delivered,
303
+ Cancelled
304
+ }
305
+ ```
306
+
307
+ For enums that need string serialization (APIs, databases), configure the JSON serializer to use string names rather than integer values. Don't scatter `ToString()` / `Enum.Parse()` calls through business logic.
308
+
309
+ ## Imports
310
+
311
+ Use `global using` directives (C# 10+) in a single `GlobalUsings.cs` file for project-wide imports. Don't duplicate common usings across every file. Match existing project convention — if there's no `GlobalUsings.cs`, use regular per-file usings.
312
+
313
+ Order: `System.*`, third-party, project namespaces. Let the IDE/formatter sort them.
314
+
315
+ ## Logging
316
+
317
+ Use `ILogger<T>` with structured logging. Use message templates with named placeholders — never string interpolation:
318
+
319
+ ```csharp
320
+ // Wrong — interpolation defeats structured logging
321
+ logger.LogInformation($"User {userId} created order {orderId}");
322
+
323
+ // Correct — structured, searchable, parameterized
324
+ logger.LogInformation("User {UserId} created order {OrderId}", userId, orderId);
325
+ ```
326
+
327
+ For hot paths, use `LoggerMessage.Define` to avoid allocation:
328
+
329
+ ```csharp
330
+ private static readonly Action<ILogger, int, Exception?> LogUserCreated =
331
+ LoggerMessage.Define<int>(LogLevel.Information, new EventId(1), "User {UserId} created");
332
+
333
+ // Usage
334
+ LogUserCreated(logger, user.Id, null);
335
+ ```
336
+
337
+ Or with .NET 6+ source generators:
338
+
339
+ ```csharp
340
+ [LoggerMessage(Level = LogLevel.Information, Message = "User {UserId} created")]
341
+ private static partial void LogUserCreated(ILogger logger, int userId);
342
+ ```
343
+
344
+ ## Testing
345
+
346
+ Match the project's test runner (xUnit, NUnit, MSTest) and mocking library (Moq, NSubstitute, FakeItEasy).
347
+
348
+ When to mock: external HTTP APIs, databases, third-party services, and anything with side effects or costs. When to use real instances: pure logic, in-memory implementations, value objects.
349
+
350
+ Test behavior, not implementation — test what a method returns or what side effects it produces, not how it internally works.
351
+
352
+ ```csharp
353
+ [Test]
354
+ public async Task GetUser_WhenExists_ReturnsUser()
355
+ {
356
+ // Arrange
357
+ var repository = Substitute.For<IUserRepository>();
358
+ repository.GetByIdAsync(42, Arg.Any<CancellationToken>())
359
+ .Returns(new User { Id = 42, Name = "Alice" });
360
+ var service = new UserService(repository, NullLogger<UserService>.Instance);
361
+
362
+ // Act
363
+ var result = await service.GetByIdAsync(42, CancellationToken.None);
364
+
365
+ // Assert
366
+ Assert.That(result.Name, Is.EqualTo("Alice"));
367
+ }
368
+ ```