{
  "schema": "https://ai-atoms.com/schemas/skill-v1.json",
  "type": "skill",
  "id": "skill/generate-testability-wrappers",
  "version": "1.0.1",
  "name": "generate-testability-wrappers",
  "description": "DO NOT USE when the target already consumes an injected interface or built-in abstraction such as IFileSystem or TimeProvider, even if the request says \"generate a wrapper\"; no new wrapper is needed. Use only when C# source calls an ambient/static dependency and no injectable seam exists: first-time TimeProvider, IHttpClientFactory, or System.IO.Abstractions adoption; minimal Environment/Console/Process wrappers; IProcessRunner; DI registration; or an ambient seam that preserves a static API. Exclude static detection (detect-static-dependencies), migration to an existing/registered abstraction (migrate-static-to-wrapper), one blocked behavior plus deterministic tests (testability-obstacle), and general interface design.",
  "system_prompt_fragment": "# Generate Testability Wrappers\n\nGenerate wrapper interfaces, default implementations, and DI service registration code for untestable static dependencies. For statics that already have .NET built-in abstractions (`TimeProvider`, `IHttpClientFactory`), guide adoption of the built-in. For statics without built-in alternatives, generate custom minimal wrappers.\n\n## When to Use\n\n- After running `detect-static-dependencies` and identifying which statics to wrap\n- When the user asks to make a class testable by replacing statics with injected abstractions\n- When adopting `TimeProvider` (.NET 8+) or `System.IO.Abstractions`\n- When creating a custom wrapper for `Environment.*`, `Console.*`, or `Process.*`\n\n## When Not to Use\n\n- The user wants to find statics first (use `detect-static-dependencies`)\n- The user wants to bulk-replace call sites (use `migrate-static-to-wrapper`)\n- The static is already behind an interface\n- The project does not use dependency injection and the user does not want to add it\n\n## Inputs\n\n| Input | Required | Description |\n|-------|----------|-------------|\n| Static category | Yes | Which category: `time`, `filesystem`, `environment`, `network`, `console`, `process` |\n| Target framework | Yes | The `TargetFramework` from `.csproj` (affects which built-in abstractions exist) |\n| DI container | No | Which DI framework: `microsoft` (default), `autofac`, `none` (ambient context) |\n| Namespace | No | Target namespace for generated wrapper code |\n\n## Workflow\n\n### Step 1: Determine the abstraction strategy\n\nBased on the category and target framework:\n\n| Category | .NET 8+ | .NET 6-7 | .NET Framework |\n|----------|---------|----------|----------------|\n| Time | `TimeProvider` (built-in) | `TimeProvider` via `Microsoft.Bcl.TimeProvider` NuGet | Custom `ISystemClock` |\n| File system | `System.IO.Abstractions` (NuGet) | Same | Same |\n| HTTP | `IHttpClientFactory` (built-in) | Same | Same |\n| Environment | Custom `IEnvironmentProvider` | Same | Same |\n| Console | Custom `IConsole` | Same | Same |\n| Process | Custom `IProcessRunner` | Same | Same |\n\n### Step 2: Generate built-in abstraction adoption (Time, HTTP)\n\n#### TimeProvider (.NET 8+)\n\nNo wrapper code needed — guide the user:\n\n1. Register in DI:\n```csharp\nbuilder.Services.AddSingleton(TimeProvider.System);\n```\n\n2. Inject into classes:\n```csharp\npublic class OrderProcessor(TimeProvider timeProvider)\n{\n    public bool IsExpired(Order order)\n        => timeProvider.GetUtcNow() > order.ExpiresAt;\n}\n```\n\n3. Test with `FakeTimeProvider`:\n```csharp\n// Requires Microsoft.Extensions.TimeProvider.Testing NuGet\nvar fakeTime = new FakeTimeProvider(new DateTimeOffset(2026, 1, 15, 0, 0, 0, TimeSpan.Zero));\nvar processor = new OrderProcessor(fakeTime);\nfakeTime.Advance(TimeSpan.FromDays(1));\nAssert.True(processor.IsExpired(order));\n```\n\n#### TimeProvider (pre-.NET 8)\n\nGuide: install `Microsoft.Bcl.TimeProvider` NuGet. Same API as above.\n\n#### IHttpClientFactory\n\nNo wrapper code needed — register typed clients via `builder.Services.AddHttpClient<MyService>()` and inject `HttpClient` directly into the class constructor.\n\n### Step 3: Generate custom wrappers (Environment, Console, Process)\n\nFor categories without built-in abstractions, follow this template:\n\n#### Interface — define the minimal surface\n\nOnly include methods that were actually detected in the codebase. Do NOT generate a wrapper for every possible member — wrap only what is used.\n\n```csharp\nnamespace <Namespace>;\n\n/// <summary>\n/// Abstraction over <static class> for testability. \n/// </summary>\npublic interface I<WrapperName>\n{\n    // One method per detected static call\n    <return type> <MethodName>(<parameters>);\n}\n```\n\n#### Default implementation — delegate to the real static\n\n```csharp\nnamespace <Namespace>;\n\n/// <summary>\n/// Default implementation that delegates to <static class>.\n/// </summary>\npublic sealed class <WrapperName> : I<WrapperName>\n{\n    public <return type> <MethodName>(<parameters>)\n        => <StaticClass>.<Method>(<arguments>);\n}\n```\n\n#### DI registration\n\n```csharp\n// In Program.cs or Startup.cs:\nbuilder.Services.AddSingleton<I<WrapperName>, <WrapperName>>();\n```\n\n### Step 4: Generate file system wrapper adoption\n\nPrefer the established `System.IO.Abstractions` NuGet package over custom wrappers:\n\n1. Install the package:\n```\ndotnet add package System.IO.Abstractions\n```\n\n2. Register in DI:\n```csharp\nbuilder.Services.AddSingleton<IFileSystem, FileSystem>();\n```\n\n3. Inject `IFileSystem` into classes:\n```csharp\npublic class ConfigLoader(IFileSystem fileSystem)\n{\n    public string LoadConfig(string path)\n        => fileSystem.File.ReadAllText(path);\n}\n```\n\n4. Test with `MockFileSystem`:\n```\ndotnet add <TestProject> package System.IO.Abstractions.TestingHelpers\n```\n```csharp\nvar mockFs = new MockFileSystem(new Dictionary<string, MockFileData>\n{\n    { \"/config.json\", new MockFileData(\"{\\\"key\\\": \\\"value\\\"}\") }\n});\nvar loader = new ConfigLoader(mockFs);\nAssert.Equal(\"{\\\"key\\\": \\\"value\\\"}\", loader.LoadConfig(\"/config.json\"));\n```\n\n### Step 5: Generate ambient context alternative (when DI is not available)\n\nIf the codebase does not use DI (e.g., old console app, library code), offer the ambient context pattern:\n\n```csharp\npublic static class Clock\n{\n    private static readonly AsyncLocal<Func<DateTimeOffset>?> s_override = new();\n    public static DateTimeOffset UtcNow\n        => s_override.Value?.Invoke() ?? TimeProvider.System.GetUtcNow();\n\n    public static IDisposable Override(DateTimeOffset fixedTime)\n    {\n        s_override.Value = () => fixedTime;\n        return new Scope();\n    }\n    private sealed class Scope : IDisposable\n    {\n        public void Dispose() => s_override.Value = null;\n    }\n}\n```\n\nKey trade-offs: `AsyncLocal<T>` ensures parallel tests don't interfere; production cost is one null check per call; the `static readonly` field is essentially free.\n\n### Step 6: Place generated files\n\nGenerate files following the project's existing conventions:\n- If there is an `Abstractions/` or `Interfaces/` folder, place the interface there\n- If there is an `Infrastructure/` or `Services/` folder, place the implementation there\n- Otherwise, create files next to the code that uses the static\n\nAlways generate:\n1. The interface file (or adoption instructions for built-in abstractions)\n2. The default implementation file\n3. The DI registration snippet (as a code comment at the bottom of the implementation, or as separate instructions)\n\n## Validation\n\n- [ ] Generated interface only wraps statics that were actually detected (not speculative)\n- [ ] Default implementation delegates to the real static with no behavior changes\n- [ ] DI registration uses `AddSingleton` for stateless wrappers, `AddTransient` for stateful ones\n- [ ] NuGet packages are recommended where established libraries exist (System.IO.Abstractions, etc.)\n- [ ] For .NET 8+, `TimeProvider` is recommended over custom `ISystemClock`\n- [ ] Ambient context pattern includes `AsyncLocal<T>`, scoped disposal, and trade-off explanation\n\n## Common Pitfalls\n\n| Pitfall | Solution |\n|---------|----------|\n| Wrapping ALL members of a static class | Only wrap methods actually called in the codebase |\n| Custom time wrapper on .NET 8+ | Use built-in `TimeProvider` instead |\n| Custom file system wrapper | Prefer `System.IO.Abstractions` NuGet — battle-tested, complete |\n| Registering scoped when singleton suffices | Stateless wrappers should be `AddSingleton` |\n| Forgetting test helper packages | `Microsoft.Extensions.TimeProvider.Testing` for time, `System.IO.Abstractions.TestingHelpers` for filesystem |\n| Ambient context without `AsyncLocal` | Non-async `[ThreadStatic]` breaks with `async`/`await` — always use `AsyncLocal<T>` |",
  "applicable_domains": [
    "code",
    "dotnet",
    "engineering"
  ],
  "invocation": [
    "/generate-testability-wrappers"
  ],
  "tags": [
    "dotnet-test",
    "dotnet",
    "csharp",
    "microsoft"
  ],
  "authored_by": "anthropics",
  "source_url": "https://github.com/dotnet/skills/blob/main/plugins/dotnet-test/skills/generate-testability-wrappers/SKILL.md",
  "lifecycle": "stable",
  "category": "dotnet",
  "provenance": {
    "source": "dotnet/skills",
    "source_url": "https://github.com/dotnet/skills/blob/main/plugins/dotnet-test/skills/generate-testability-wrappers/SKILL.md",
    "author": "Microsoft / .NET Foundation",
    "license": "MIT",
    "notes": "Imported by scripts/import-anthropic-skills.py."
  }
}