Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions .github/agents/test.agent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
---
description: "Use when writing, fixing, or reviewing tests for WB.Configuration; run tests, analyze failures, and keep changes confined to /tests/."
name: "Test Writer Agent"
tools: [read, search, execute, edit, todo]
user-invocable: true
---

You are a test writer.

## Constraints
- ONLY write, edit, or delete files under /tests/.
- NEVER modify source code under /src/ or any non-test files.
- If failure analysis reveals the root cause is a defect in /src/, do not modify source code. Instead, document the finding in your output, mark the test with a comment noting the upstream defect, and leave the test in place so the failure is visible.
- NEVER remove failing tests just to make the suite pass.
- ALWAYS preserve or improve test clarity, determinism, and coverage.

## Responsibilities
- Write tests for this codebase.
- After making changes, first run only the test class(es) you modified. If those pass, run all tests under /tests/ to check for regressions.
- If the test runner fails to execute (e.g., build errors, missing tooling), report the exact error output, do not assume test results, and stop further edits until the environment issue is resolved.
- Analyze failures and explain the most likely cause with concrete evidence from the test output.
- Prefer the smallest test change that proves the behavior.

## Test Structure Standards
- Follow the existing arrange, act, assert style used in this repository.
- Prefer one behavior per test method.
- Use descriptive test names that state the expected outcome.
- Keep assertions focused and readable.
- Mirror the repository's current style of `AwesomeAssertions`, `[Test]`, and explicit namespaces.
- Use one sub directory per class under test.
- Use `MethodTests` subdirectories for method-specific tests.
- Use `PropertyTests` subdirectories for property-specific tests.

## Good Test Structure Examples
```csharp
[Test]
public void ShouldReturnValue_WhenKeyExists()
{
// Arrange
IConfigurationNode configurationNode = new ConfigurationNode(new JsonObject
{
["key"] = "value"
});

// Act
string value = configurationNode.GetRequired<string>("key");

// Assert
value.Should().Be("value");
}
```

```csharp
[Test]
public void ShouldThrowArgumentOutOfRangeException_WhenIndexDoesNotExist()
{
// Arrange
IConfigurationNode configurationNode = new ConfigurationNode(new JsonArray());

// Act
Action act = () => configurationNode.GetRequired<string>(0);

// Assert
act.Should().Throw<ArgumentOutOfRangeException>();
}
```

## Approach
1. Inspect the nearest existing test file and match its naming and structure.
2. Add or adjust tests only inside /tests/.
3. Run the narrowest relevant test set first, then broaden only if needed.
4. Report what passed, what failed, and what the failures imply.

## Execute Tests
- Tests are run with Microsoft.Testing.Platform.
- Use `dotnet run` with appropriate filters to run specific test classes or methods.

## Output Format
- Summarize the test files changed.
- List the test commands you ran.
- Report failures with their likely cause.
- State any remaining risk if coverage is still incomplete.
8 changes: 8 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# AGENTS.md

## Commit Message Rules

- Use Conventional Commits
- Imperative mood
- Max 72 chars in subject
- Wrap body at 100 chars
26 changes: 26 additions & 0 deletions src/Configuration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
namespace WB.Configuration;

/// <inheritdoc cref="IConfiguration"/>
public sealed class Configuration : IConfiguration, IConfigurationNode

Check warning on line 9 in src/Configuration.cs

View workflow job for this annotation

GitHub Actions / 🏗️ Build Nuget Package / build

The type name Configuration conflicts in whole or in part with the namespace name 'System.Configuration' defined in the .NET Framework. Rename the type to eliminate the conflict. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1724)

Check warning on line 9 in src/Configuration.cs

View workflow job for this annotation

GitHub Actions / 🏗️ Build Nuget Package / build

The type name Configuration conflicts in whole or in part with the namespace name 'System.Configuration' defined in the .NET Framework. Rename the type to eliminate the conflict. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1724)

Check warning on line 9 in src/Configuration.cs

View workflow job for this annotation

GitHub Actions / ✅ Run Tests / ConfigurationTests

The type name Configuration conflicts in whole or in part with the namespace name 'System.Configuration' defined in the .NET Framework. Rename the type to eliminate the conflict. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1724)

Check warning on line 9 in src/Configuration.cs

View workflow job for this annotation

GitHub Actions / ✅ Run Tests / ConfigurationNodeTests

The type name Configuration conflicts in whole or in part with the namespace name 'System.Configuration' defined in the .NET Framework. Rename the type to eliminate the conflict. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1724)

Check warning on line 9 in src/Configuration.cs

View workflow job for this annotation

GitHub Actions / ✅ Run Tests / IConfigurationTests

The type name Configuration conflicts in whole or in part with the namespace name 'System.Configuration' defined in the .NET Framework. Rename the type to eliminate the conflict. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1724)
{
// ┌─────────────────────────────────────────────────────────────────────────────┐
// │ Private Fields │
Expand All @@ -17,6 +17,32 @@

private ConfigurationNode? configurationNode;

public IConfigurationNode this[int index]

Check warning on line 20 in src/Configuration.cs

View workflow job for this annotation

GitHub Actions / 🏗️ Build Nuget Package / build

Missing XML comment for publicly visible type or member 'Configuration.this[int]'

Check warning on line 20 in src/Configuration.cs

View workflow job for this annotation

GitHub Actions / 🏗️ Build Nuget Package / build

Missing XML comment for publicly visible type or member 'Configuration.this[int]'

Check warning on line 20 in src/Configuration.cs

View workflow job for this annotation

GitHub Actions / ✅ Run Tests / ConfigurationTests

Missing XML comment for publicly visible type or member 'Configuration.this[int]'

Check warning on line 20 in src/Configuration.cs

View workflow job for this annotation

GitHub Actions / ✅ Run Tests / ConfigurationNodeTests

Missing XML comment for publicly visible type or member 'Configuration.this[int]'

Check warning on line 20 in src/Configuration.cs

View workflow job for this annotation

GitHub Actions / ✅ Run Tests / IConfigurationTests

Missing XML comment for publicly visible type or member 'Configuration.this[int]'
{
get
{
if (configurationNode is null)
{
return new ConfigurationNode(null);
}

return configurationNode[index];
}
}

public IConfigurationNode this[string key]

Check warning on line 33 in src/Configuration.cs

View workflow job for this annotation

GitHub Actions / 🏗️ Build Nuget Package / build

Missing XML comment for publicly visible type or member 'Configuration.this[string]'

Check warning on line 33 in src/Configuration.cs

View workflow job for this annotation

GitHub Actions / 🏗️ Build Nuget Package / build

Missing XML comment for publicly visible type or member 'Configuration.this[string]'

Check warning on line 33 in src/Configuration.cs

View workflow job for this annotation

GitHub Actions / ✅ Run Tests / ConfigurationTests

Missing XML comment for publicly visible type or member 'Configuration.this[string]'

Check warning on line 33 in src/Configuration.cs

View workflow job for this annotation

GitHub Actions / ✅ Run Tests / ConfigurationNodeTests

Missing XML comment for publicly visible type or member 'Configuration.this[string]'

Check warning on line 33 in src/Configuration.cs

View workflow job for this annotation

GitHub Actions / ✅ Run Tests / IConfigurationTests

Missing XML comment for publicly visible type or member 'Configuration.this[string]'
{
get
{
if (configurationNode is null)
{
return new ConfigurationNode(null);
}

return configurationNode[key];
}
}

// ┌─────────────────────────────────────────────────────────────────────────────┐
// │ Public Methods │
// └─────────────────────────────────────────────────────────────────────────────┘
Expand Down
41 changes: 38 additions & 3 deletions src/ConfigurationNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,49 @@ internal sealed class ConfigurationNode(JsonNode? jsonNode) : IConfigurationNode
// └─────────────────────────────────────────────────────────────────────────────┘
private readonly JsonNode? jsonNode = jsonNode;

// ┌─────────────────────────────────────────────────────────────────────────────┐
// │ Public Indexers │
// └─────────────────────────────────────────────────────────────────────────────┘

/// <inheritdoc />
public IConfigurationNode this[string key]
{
get
{
if (jsonNode is JsonObject jsonObject && jsonObject.TryGetPropertyValue(key, out JsonNode? childNode))
{
return new ConfigurationNode(childNode);
}
else
{
return new ConfigurationNode(null);
}
}
}

public IConfigurationNode this[int index]
{
get
{
if (jsonNode is JsonArray jsonArray && index >= 0 && index < jsonArray.Count)
{
return new ConfigurationNode(jsonArray[index]);
}
else
{
return new ConfigurationNode(null);
}
}
}

// ┌─────────────────────────────────────────────────────────────────────────────┐
// │ Public Methods │
// └─────────────────────────────────────────────────────────────────────────────┘

/// <inheritdoc />
public bool TryGet<T>(string key, out T? value)
{
if (jsonNode is JsonObject obj && obj.TryGetPropertyValue(key, out JsonNode? jsonValue))
if (jsonNode is JsonObject jsonObject && jsonObject.TryGetPropertyValue(key, out JsonNode? jsonValue))
{
value = jsonValue.Deserialize<T>();

Expand All @@ -35,9 +70,9 @@ public bool TryGet<T>(string key, out T? value)
/// <inheritdoc />
public bool TryGet<T>(int index, out T? value)
{
if (jsonNode is JsonArray arr && index >= 0 && index < arr.Count)
if (jsonNode is JsonArray jsonArray && index >= 0 && index < jsonArray.Count)
{
value = arr[index].Deserialize<T>();
value = jsonArray[index].Deserialize<T>();

return true;
}
Expand Down
18 changes: 18 additions & 0 deletions src/IConfigurationNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,24 @@ namespace WB.Configuration;
/// </summary>
public interface IConfigurationNode
{
// ┌─────────────────────────────────────────────────────────────────────────────┐
// │ Public Indexers │
// └─────────────────────────────────────────────────────────────────────────────┘

/// <summary>
/// Gets a child configuration node associated with the specified key. If the key does not exist, returns an empty configuration node that will return default values for any queries.
/// </summary>
/// <param name="key">The key associated with the child configuration node.</param>
/// <returns>A child configuration node associated with the specified key, or an empty configuration node if the key does not exist.</returns>
public IConfigurationNode this[string key] { get; }

/// <summary>
/// Gets a child configuration node at the specified index. If the index is out of range, returns an empty configuration node that will return default values for any queries.
/// </summary>
/// <param name="index">The index of the child configuration node.</param>
/// <returns>A child configuration node at the specified index, or an empty configuration node if the index is out of range.</returns>
public IConfigurationNode this[int index] { get; }

// ┌─────────────────────────────────────────────────────────────────────────────┐
// │ Public Methods │
// └─────────────────────────────────────────────────────────────────────────────┘
Expand Down
67 changes: 67 additions & 0 deletions tests/ConfigurationNodeTests/src/IndexerTests/IntIndexTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using System.Text.Json.Nodes;
using AwesomeAssertions;
using WB.Configuration;

namespace ConfigurationNodeTests.IndexerTests.IntIndexTests;

public sealed class IntIndexTests
{
[Test]
public void ShouldReturnChildConfigurationNode_WhenIndexExists()
{
// Arrange
ConfigurationNode configurationNode = new(new JsonArray
{
new JsonObject
{
["key"] = "value"
}
});

// Act
IConfigurationNode childNode = configurationNode[0];
string? value = childNode.Get<string>("key");

// Assert
value.Should().Be("value", because: "the index exists and should return the child configuration node");
}

[Test]
public void ShouldReturnEmptyConfigurationNode_WhenIndexDoesNotExist()
{
// Arrange
ConfigurationNode configurationNode = new(new JsonArray
{
new JsonObject
{
["key"] = "value"
}
});

// Act
IConfigurationNode childNode = configurationNode[1];
bool result = childNode.TryGet("key", out string? value);

// Assert
result.Should().BeFalse(because: "the index does not exist in the current configuration node");
value.Should().BeNull(because: "an empty configuration node returns default values for any query");
}

[Test]
public void ShouldReturnEmptyConfigurationNode_WhenCurrentNodeIsNotArray()
{
// Arrange
ConfigurationNode configurationNode = new(new JsonObject
{
["key"] = "value"
});

// Act
IConfigurationNode childNode = configurationNode[0];
bool result = childNode.TryGet("key", out string? value);

// Assert
result.Should().BeFalse(because: "int index lookup requires the current node to be a JSON array");
value.Should().BeNull(because: "non-array nodes return an empty configuration node for int index lookups");
}
}
64 changes: 64 additions & 0 deletions tests/ConfigurationNodeTests/src/IndexerTests/StringIndexTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using System.Text.Json.Nodes;
using AwesomeAssertions;
using WB.Configuration;

namespace ConfigurationNodeTests.IndexerTests.StringIndexTests;

public sealed class StringIndexTests
{
[Test]
public void ShouldReturnChildConfigurationNode_WhenKeyExists()
{
// Arrange
ConfigurationNode configurationNode = new(new JsonObject
{
["section"] = new JsonObject
{
["key"] = "value"
}
});

// Act
IConfigurationNode childNode = configurationNode["section"];
string? value = childNode.Get<string>("key");

// Assert
value.Should().Be("value", because: "the key exists and should return the child configuration node");
}

[Test]
public void ShouldReturnEmptyConfigurationNode_WhenKeyDoesNotExist()
{
// Arrange
ConfigurationNode configurationNode = new(new JsonObject
{
["section"] = new JsonObject
{
["key"] = "value"
}
});

// Act
IConfigurationNode childNode = configurationNode["nonexistentSection"];
bool result = childNode.TryGet("key", out string? value);

// Assert
result.Should().BeFalse(because: "the key does not exist in the current configuration node");
value.Should().BeNull(because: "an empty configuration node returns default values for any query");
}

[Test]
public void ShouldReturnEmptyConfigurationNode_WhenCurrentNodeIsNotObject()
{
// Arrange
ConfigurationNode configurationNode = new(new JsonArray("value"));

// Act
IConfigurationNode childNode = configurationNode["section"];
bool result = childNode.TryGet("key", out string? value);

// Assert
result.Should().BeFalse(because: "string index lookup requires the current node to be a JSON object");
value.Should().BeNull(because: "non-object nodes return an empty configuration node for string index lookups");
}
}
Loading