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
27 changes: 18 additions & 9 deletions docs/packages/compono-testdoubles.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,15 @@ dotnet add package Compono.TestDoubles --prerelease

`Compono.TestDoubles` is not a general-purpose mocking framework — see
[ADR-0042](../adr/0042-compono-owned-source-generated-test-doubles.md)'s
Non-Goals. If you need call verification, argument matchers, or a familiar
runtime-proxy substitute, use
[`Compono.NSubstitute`](compono-nsubstitute.md) instead; the two packages
are not mutually exclusive.
Non-Goals — but current generated doubles do support `Configure()`,
`Verify()`, literal equality matching, `Match.Any<T>()`,
`Match.Is<T>(predicate)`, argument-filtered `Never()`/`Once()`/`Exactly(n)`,
and multi-entry argument-distinguished response configuration for eligible
member shapes. Use [`Compono.NSubstitute`](compono-nsubstitute.md) when you
intentionally want a familiar runtime-proxy substitute or a capability still
outside generated-double support, such as invocation-aware callbacks, true
argument capture, call-order verification, or partial/strict substitutes;
the two packages are not mutually exclusive.

## Compile-time opt-in

Expand Down Expand Up @@ -71,10 +76,12 @@ service.Repository.Configure().CountAsync().Returns(Task.FromResult(4));
`using` or a generation failure — see
[ADR-0043 Amendment 3 Finding C](../adr/0043-compono-generated-test-doubles-design.md#amendment-3-2026-08-13-public-cross-assembly-state-contract-overloadname-collision-diagnostics-documented-multi-assembly-registry-limitation).
- **Per-member `.Returns(...)`/`.Throws(...)`** — configure a method or
property's behavior; last configuration wins (calling `.Returns(...)`
after an earlier `.Throws(...)` on the same member clears the exception,
and vice versa). Configuration is member-level and **argument-
independent** — there are no argument matchers in v1.
property's behavior. A zero-argument `Configure().Member()` applies to
every call to that member. For eligible parameterized members,
`Configure().Member(...)` accepts literal equality arguments,
`Match.Any<T>()`, and `Match.Is<T>(predicate)`; multiple argument-
distinguished configurations can coexist, with the most recently
registered matching entry winning.
- **Deterministic defaults for unconfigured members** — primitives,
nullable references, `Task`/`Task<T>`, `ValueTask`/`ValueTask<T>`, and
known collection shapes (arrays, `List<T>`, `Dictionary<TKey,TValue>`,
Expand Down Expand Up @@ -664,4 +671,6 @@ An unsupported member shape is a compile-time diagnostic
- [Providers](../concepts/providers.md) — where the generated-double
provider sits in the resolution pipeline.
- [`Compono.NSubstitute`](compono-nsubstitute.md) — the runtime-proxy
alternative, for call verification/argument matchers.
alternative, for capabilities still outside generated-double support
(for example invocation-aware callbacks, true argument capture,
call-order verification, or partial/strict substitutes).
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@

## CallVerifier\(int, string\) Constructor

Asserts how many times a generated test double's member was called, backed by
[ConfiguredCallCount](Compono.ReturnConfig_T_.ConfiguredCallCount.md 'Compono\.ReturnConfig\<T\>\.ConfiguredCallCount')\. Deliberately minimal \- [Never\(\)](Compono.CallVerifier.Never().md 'Compono\.CallVerifier\.Never\(\)')/
[Once\(\)](Compono.CallVerifier.Once().md 'Compono\.CallVerifier\.Once\(\)')/[Exactly\(int\)](Compono.CallVerifier.Exactly(int).md 'Compono\.CallVerifier\.Exactly\(int\)') only, no argument matchers, no call\-order verification,
per ADR\-0044 Requirement 3\.
Performs terminal call\-count assertions after any generated member and argument filtering has
already been applied\. Deliberately minimal \- [Never\(\)](Compono.CallVerifier.Never().md 'Compono\.CallVerifier\.Never\(\)')/[Once\(\)](Compono.CallVerifier.Once().md 'Compono\.CallVerifier\.Once\(\)')/
[Exactly\(int\)](Compono.CallVerifier.Exactly(int).md 'Compono\.CallVerifier\.Exactly\(int\)') only, with no call\-order verification, per ADR\-0044 Requirement 3\.

```csharp
public CallVerifier(int observedCount, string memberDescription);
Expand All @@ -17,7 +16,7 @@ public CallVerifier(int observedCount, string memberDescription);

`observedCount` [System\.Int32](https://learn.microsoft.com/en-us/dotnet/api/system.int32 'System\.Int32')

How many times the member's dispatch body actually ran\.
How many matching calls the generated verification surface observed\.

<a name='Compono.CallVerifier.CallVerifier(int,string).memberDescription'></a>

Expand Down
9 changes: 4 additions & 5 deletions docs/reference/api/Compono/Compono.CallVerifier.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,17 @@

## CallVerifier Struct

Asserts how many times a generated test double's member was called, backed by
[ConfiguredCallCount](Compono.ReturnConfig_T_.ConfiguredCallCount.md 'Compono\.ReturnConfig\<T\>\.ConfiguredCallCount')\. Deliberately minimal \- [Never\(\)](Compono.CallVerifier.Never().md 'Compono\.CallVerifier\.Never\(\)')/
[Once\(\)](Compono.CallVerifier.Once().md 'Compono\.CallVerifier\.Once\(\)')/[Exactly\(int\)](Compono.CallVerifier.Exactly(int).md 'Compono\.CallVerifier\.Exactly\(int\)') only, no argument matchers, no call\-order verification,
per ADR\-0044 Requirement 3\.
Performs terminal call\-count assertions after any generated member and argument filtering has
already been applied\. Deliberately minimal \- [Never\(\)](Compono.CallVerifier.Never().md 'Compono\.CallVerifier\.Never\(\)')/[Once\(\)](Compono.CallVerifier.Once().md 'Compono\.CallVerifier\.Once\(\)')/
[Exactly\(int\)](Compono.CallVerifier.Exactly(int).md 'Compono\.CallVerifier\.Exactly\(int\)') only, with no call\-order verification, per ADR\-0044 Requirement 3\.

```csharp
public readonly struct CallVerifier
```

| Constructors | |
| :--- | :--- |
| [CallVerifier\(int, string\)](Compono.CallVerifier.CallVerifier(int,string).md 'Compono\.CallVerifier\.CallVerifier\(int, string\)') | Asserts how many times a generated test double's member was called, backed by [ConfiguredCallCount](Compono.ReturnConfig_T_.ConfiguredCallCount.md 'Compono\.ReturnConfig\<T\>\.ConfiguredCallCount')\. Deliberately minimal \- [Never\(\)](Compono.CallVerifier.Never().md 'Compono\.CallVerifier\.Never\(\)')/ [Once\(\)](Compono.CallVerifier.Once().md 'Compono\.CallVerifier\.Once\(\)')/[Exactly\(int\)](Compono.CallVerifier.Exactly(int).md 'Compono\.CallVerifier\.Exactly\(int\)') only, no argument matchers, no call\-order verification, per ADR\-0044 Requirement 3\. |
| [CallVerifier\(int, string\)](Compono.CallVerifier.CallVerifier(int,string).md 'Compono\.CallVerifier\.CallVerifier\(int, string\)') | Performs terminal call\-count assertions after any generated member and argument filtering has already been applied\. Deliberately minimal \- [Never\(\)](Compono.CallVerifier.Never().md 'Compono\.CallVerifier\.Never\(\)')/[Once\(\)](Compono.CallVerifier.Once().md 'Compono\.CallVerifier\.Once\(\)')/ [Exactly\(int\)](Compono.CallVerifier.Exactly(int).md 'Compono\.CallVerifier\.Exactly\(int\)') only, with no call\-order verification, per ADR\-0044 Requirement 3\. |

| Methods | |
| :--- | :--- |
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/api/Compono/Compono.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@

| Structs | |
| :--- | :--- |
| [CallVerifier](Compono.CallVerifier.md 'Compono\.CallVerifier') | Asserts how many times a generated test double's member was called, backed by [ConfiguredCallCount](Compono.ReturnConfig_T_.ConfiguredCallCount.md 'Compono\.ReturnConfig\<T\>\.ConfiguredCallCount')\. Deliberately minimal \- [Never\(\)](Compono.CallVerifier.Never().md 'Compono\.CallVerifier\.Never\(\)')/ [Once\(\)](Compono.CallVerifier.Once().md 'Compono\.CallVerifier\.Once\(\)')/[Exactly\(int\)](Compono.CallVerifier.Exactly(int).md 'Compono\.CallVerifier\.Exactly\(int\)') only, no argument matchers, no call\-order verification, per ADR\-0044 Requirement 3\. |
| [CallVerifier](Compono.CallVerifier.md 'Compono\.CallVerifier') | Performs terminal call\-count assertions after any generated member and argument filtering has already been applied\. Deliberately minimal \- [Never\(\)](Compono.CallVerifier.Never().md 'Compono\.CallVerifier\.Never\(\)')/[Once\(\)](Compono.CallVerifier.Once().md 'Compono\.CallVerifier\.Once\(\)')/ [Exactly\(int\)](Compono.CallVerifier.Exactly(int).md 'Compono\.CallVerifier\.Exactly\(int\)') only, with no call\-order verification, per ADR\-0044 Requirement 3\. |
| [CompositionProviderRequest](Compono.CompositionProviderRequest.md 'Compono\.CompositionProviderRequest') | A composition request, as seen by a public [ICompositionValueProvider](Compono.ICompositionValueProvider.md 'Compono\.ICompositionValueProvider') \- decoupled from the engine's own internal `Compono.CompositionRequest` \(no path, no shared\-scope flag, no pipeline plumbing a provider author has no legitimate use for\)\. See `docs/adr/0024-public-provider-extensibility-model.md`\. |
| [CompositionProviderResult](Compono.CompositionProviderResult.md 'Compono\.CompositionProviderResult') | What an [ICompositionValueProvider](Compono.ICompositionValueProvider.md 'Compono\.ICompositionValueProvider') reports for one [CompositionProviderRequest](Compono.CompositionProviderRequest.md 'Compono\.CompositionProviderRequest')\. |
| [CompositionRequestDescriptor](Compono.CompositionRequestDescriptor.md 'Compono\.CompositionRequestDescriptor') | The compact, compile\-time\-constructible value a generated [ICompositionPlan&lt;T&gt;](Compono.ICompositionPlan_T_.md 'Compono\.ICompositionPlan\<T\>') passes to [Resolve&lt;TValue&gt;\(CompositionRequestDescriptor\)](Compono.ICompositionContext.Resolve.md#Compono.ICompositionContext.Resolve_TValue_(Compono.CompositionRequestDescriptor) 'Compono\.ICompositionContext\.Resolve\<TValue\>\(Compono\.CompositionRequestDescriptor\)') for one constructor parameter or required member \- or, for a test\-framework integration composing a [CompositionRow](Compono.CompositionRow.md 'Compono\.CompositionRow'), one of a test method's own parameters \([TestParameter](Compono.CompositionRequestKind.md#Compono.CompositionRequestKind.TestParameter 'Compono\.CompositionRequestKind\.TestParameter')\)\. |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# [RESEARCH-0012] AWS Secrets Manager Provider Compono Skill Regression

**Status:** Done

**Date:** 2026-08-27

## Scope

This records two focused follow-up findings from the real AWS Secrets Manager
Provider migration from AutoFixture/AutoFixture.Xunit3/NSubstitute to
`Compono.XunitV3` + `Compono.TestDoubles`.

This is not a product design record and creates no ADR. It applies the existing
ADR-0029/ADR-0042 evidence rules to classify what the migration actually showed.

## Finding A: installed skill missed shipped argument matching support

During the migration, the installed Compono skill initially led the agent to
write hand-made recording fakes for `IAmazonSecretsManager`,
`IConfigurationBuilder`, and `ILoggerFactory`, asserting that
`Compono.TestDoubles` intentionally did not support argument matchers/capture.
That was incorrect for ordinary matching and filtered verification.

The final migration used the shipped `Compono.TestDoubles` surface directly:

```csharp
configurationBuilder.Configure()
.Add(Match.Any<IConfigurationSource>())
.Returns(configurationBuilder);

configurationBuilder.Verify()
.Add(Match.Is<IConfigurationSource>(predicate))
.Once();
```

and:

```csharp
secretsManager.Configure()
.GetSecretValueAsync(
Match.Is<GetSecretValueRequest>(predicate),
Match.Any<CancellationToken>())
.Returns(Task.FromResult(response));
```

The migrated suite passed across `net8.0`, `net9.0`, `net10.0`, and `net11.0`
(62/62 on each TFM), with AutoFixture/NSubstitute/`Compono.NSubstitute` absent
from direct and transitive dependencies.

Classification: **skill/docs regression**, not a runtime `Compono.TestDoubles`
capability gap. Current `Compono.TestDoubles` supports literal equality
matching, `Match.Any<T>()`, `Match.Is<T>(predicate)`, argument-filtered
`Never()`/`Once()`/`Exactly(n)`, and multi-entry argument-distinguished response
configuration for eligible member shapes. True capture/callback behavior remains
a separate boundary.

## Finding B: `TestConfigurationProvider` is existing project-local test architecture

The migration retained a local `TestConfigurationProvider : ConfigurationProvider`.
This is not new evidence for abstract-class generated doubles.

Pre-migration evidence from the AWS Secrets Manager Provider test project:

```csharp
public class ConfigurationProviderSpecimenBuilder : ISpecimenBuilder
{
public object Create(object request, ISpecimenContext context)
{
if (request is Type type && type == typeof(ConfigurationProvider))
{
return new TestConfigurationProvider();
}

return new NoSpecimen();
}
}
```

```csharp
public class TestConfigurationProvider : ConfigurationProvider
{
public override void Set(string key, string value)
{
Data[key] = value;
}
}
```

The fixture registered that specimen builder before `AutoNSubstituteCustomization`:

```csharp
fixture.Customizations.Add(new ConfigurationProviderSpecimenBuilder());

fixture.Customize(new AutoNSubstituteCustomization
{
GenerateDelegates = true
});
```

Search of the pre-migration state found no `Substitute.For<ConfigurationProvider>()`
and no NSubstitute-backed substitution of `ConfigurationProvider`.

Classification: **existing project-local test architecture**, not a Compono
capability gap. Compono preserves the same design through:

```csharp
builder.Register<ConfigurationProvider>(_ => new TestConfigurationProvider());
```

ADR-0042 Amendment 2 does not apply because there is no evidenced
`Compono.NSubstitute`/NSubstitute-can vs. `Compono.TestDoubles`-cannot
replacement case. No roadmap item, ADR, or abstract-class support design is
created from this evidence.
74 changes: 74 additions & 0 deletions skills/compono-evals/benchmarks/2026-08-27/benchmark.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
{
"skill_name": "compono",
"date": "2026-08-27",
"runner": "pi --print --no-context-files --no-skills --skill <skill-path> --thinking minimal",
"scope": "Focused AWS Secrets Manager Provider TestDoubles matching/capture regression evals only",
"baseline": {
"type": "old_skill",
"snapshot_commit": "644a5ad",
"note": "Full skills/compono directory (SKILL.md + references/) snapshotted at 644a5ad, the commit before this PR's fix (5139507, caae88c). All 8 runs (4 evals x with_skill/old_skill) launched together in one batch so with_skill and old_skill are genuinely paired, per skill-creator's workflow."
},
"results": [
{
"eval_id": 28,
"name": "aws-iconfigurationbuilder-argument-filtered-verification",
"configuration": "with_skill",
"passed": true,
Comment thread
ncipollina marked this conversation as resolved.
"evidence": "Output uses Configure().Add(Match.Any<IConfigurationSource>()).Returns(configurationBuilder) and Verify().Add(Match.Is<IConfigurationSource>(...)).Once(), maps Arg.Is to Match.Is, and does not recommend a recording fake."
},
{
"eval_id": 28,
"name": "aws-iconfigurationbuilder-argument-filtered-verification",
"configuration": "old_skill",
"passed": false,
"evidence": "Output claims Compono.TestDoubles cannot express the argument predicate, uses zero-arg Configure().Add()/Verify().Add().Once(), and says asserting the argument type still requires keeping the test on NSubstitute."
},
{
"eval_id": 29,
"name": "aws-iamazonsecretsmanager-argument-matched-configuration",
"configuration": "with_skill",
"passed": true,
"evidence": "Output uses Configure().GetSecretValueAsync(Match.Is<GetSecretValueRequest>(...), Match.Any<CancellationToken>()).Returns(...) and maps Arg.Is/Arg.Any directly to Match.Is/Match.Any."
},
{
"eval_id": 29,
"name": "aws-iamazonsecretsmanager-argument-matched-configuration",
"configuration": "old_skill",
"passed": false,
"evidence": "Output drops the argument matcher entirely, uses parameterless Configure().GetSecretValueAsync().Returns(...), and states 'Compono.TestDoubles is argument-independent' explicitly."
},
{
"eval_id": 30,
"name": "adversarial-nsubstitute-vocabulary-trap",
"configuration": "with_skill",
"passed": true,
"evidence": "Output maps Arg.Is/Arg.Any/Received/DidNotReceive to Match.Is/Match.Any/Verify().Once/Exactly/Never and rejects recording fakes solely for NSubstitute vocabulary."
},
{
"eval_id": 30,
"name": "adversarial-nsubstitute-vocabulary-trap",
"configuration": "old_skill",
"passed": false,
"evidence": "Output says 'Arg.Any/Arg.Is usually disappears because generated doubles are argument-independent' and lists argument-specific returns/verification as behavior Compono.TestDoubles does not provide, contradicting the expected Arg.Is->Match.Is / Arg.Any->Match.Any mapping."
},
{
"eval_id": 31,
"name": "true-callback-capture-boundary",
"configuration": "with_skill",
"passed": true,
"evidence": "Output says ordinary matching/filtering is supported but invocation-aware callbacks/capture/delegate invocation are outside Match<T>; recommends local fake or NSubstitute seam."
},
{
"eval_id": 31,
"name": "true-callback-capture-boundary",
"configuration": "old_skill",
"passed": false,
"evidence": "Output correctly recommends a local fake for the callback boundary, but does so by claiming 'no argument matchers' / 'no argument-aware behavior' exist at all in Compono.TestDoubles, contradicting the expectation that ordinary matching/argument-filtered verification is supported."
}
],
"summary": {
"with_skill": { "passed": 4, "total": 4, "pass_rate": 1.0 },
"old_skill": { "passed": 0, "total": 4, "pass_rate": 0.0 },
"old_eval_regression_status": "not_rerun_full_benchmark_claude_weekly_limit; existing eval definitions 1-27 unchanged"
}
}
Loading
Loading