Skip to content

Add Synchronous Plugin Interaction Support - #551

Open
max-lancaster wants to merge 18 commits into
pact-foundation:masterfrom
max-lancaster:dev/plugins
Open

max-lancaster wants to merge 18 commits into
pact-foundation:masterfrom
max-lancaster:dev/plugins

Conversation

@max-lancaster

@max-lancaster max-lancaster commented Oct 7, 2025 •

Copy link
Copy Markdown

@adamrodger I took your commit in the dev/plugins branch that you referenced in Issue #492 and made a "raw" plugin implementation that should work with the PactNet.Extensions.Grpc package I created in PR #548.

There may still be some design issues to consider. One thing to note is that Plugins can have both synchronous and asynchronous interactions so I made my builder specific to the synchronous use case so we can add an asynchronous version at some point.

@adamrodger

Copy link
Copy Markdown
Contributor

I've added a commit for some cleanups and things just to save on a bunch of review comments.

I think the big design change that stands out is the two interfaces that have moved from the PactNet project to PactNet.Abstractions and made public instead of internal, and then making the port public also. Those are supposed to be implementation detail concerns so I'd prefer to keep them where they are.

We should provide some kind of generic public API if needed for plugins if they need to start a mock server instead of PactNet starting it for them at the appropriate time (but I don't see at first hand why that should happen vs. PactNet taking care of it).

@max-lancaster

Copy link
Copy Markdown
Author

I think the big design change that stands out is the two interfaces that have moved from the PactNet project to PactNet.Abstractions and made public instead of internal, and then making the port public also. Those are supposed to be implementation detail concerns so I'd prefer to keep them where they are.

These changes were introduced in order to support a public AbstractPactBuilder to share the pact verification functionality with extension library implementations. The verification functionality requires this and therefore these interfaces need to be made public in order to allow consuming libraries to use the verification functionality.

@max-lancaster

max-lancaster commented Jan 26, 2026 •

Copy link
Copy Markdown
Author

I think the big design change that stands out is the two interfaces that have moved from the PactNet project to PactNet.Abstractions and made public instead of internal, and then making the port public also. Those are supposed to be implementation detail concerns so I'd prefer to keep them where they are.

Ok, @adamrodger I took another look at this and I was able to revert the changes that made these details public and still keep my Grpc Extensions library working as intended, I think this should be ready to go now. Basically I didn't really need to inherit from AbstractPactBuilder to make the PactNet.Extensions.Grpc library work.

@mefellows

Copy link
Copy Markdown
Member

Thanks for the great work here @max-lancaster - what's needed to move this one forward @adamrodger ?

@adamrodger adamrodger left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just one API design choice to review, and then I think this is good to merge

/// </summary>
/// <param name="contentType">Content type</param>
/// <param name="content">A dictionary containing the plugin content.</param>
ISynchronousPluginRequestBuilderV4 WithContent(string contentType, Dictionary<string, object> content);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Given this returns the same object again, what happens if this is called twice? Currently this allows:

pact.UponReceiving("a request").WithContent("foo", foo).WithContent("bar", bar);

Is that intentional? Are we trying to allow setting different content types with different contents? What if we use the same content type but a different body? I think it's ambiguous whether those would combine or replace.

Other builders 'consume' the request builder when this part is called, so that you can't accidentally call it twice and thuse you avoid that ambiguity.

So I suggest maybe this returns some other type which then moves you onto the verification stage, then this has to be the last call and it can only happen once.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the request, this will replace the previous contents.

It is allowed on the response, and will append the contents (i.e. multiple responses).

So on this method, it should "consume" the builder as you describe.

@max-lancaster max-lancaster Sep 14, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @adamrodger & @mefellows, this is now addressed.

WithContent returns void and is the last call on the request builder. It can only be called once per interaction, so the ambiguity @adamrodger raised is gone. Given is still chainable before it.

I chose void rather than a new response stage after reading how pactffi_interaction_contents treats synchronous message interactions. For those, the FFI ignores the InteractionPart argument. The plugin receives one contents document and splits it into the request and the responses itself. Calling it twice replaces the request and appends a second response, exactly as @mefellows described. That means there is no separately addressable "response side" to build a stage around, and verification happens on the pact builder rather than per interaction, so the chain just ends.

Also in this update:

  • unit tests for the plugin request builder and pact builder
  • docs/plugin-interactions.md, covering the raw plugin API and how a wrapper library layers on top of it (the guidance Adam asked for on Add PactNet.Extensions.Grpc package for grpc plugin support #548)
  • rebased onto current master, and the PR base changed from dev/plugins to master so this can merge on its own

#548 remains stacked on this branch and will shrink to a single commit once this merges.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Max! Over to you Adam

adamrodger and others added 12 commits September 14, 2026 11:48
This enables creating a nicer API to expose publicly for use by plugin
wrappers.

See: pact-foundation#492
…ilder

Setting the plugin contents completes a synchronous plugin interaction, so
WithContent now returns void instead of the same builder. The plugin splits
one contents document into the request and its responses itself: a second
call would replace the request but append another response, which is why the
builder no longer allows it. Provider states stay chainable via Given.

Adds unit tests for the plugin request and pact builders and a docs page for
the raw plugin API, linked from the README.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@max-lancaster
max-lancaster changed the base branch from dev/plugins to master September 14, 2026 16:09
fabio-andre-rodrigues added a commit to fabio-andre-rodrigues/pact-net that referenced this pull request Sep 28, 2026
pact-foundation#551 reorganises Drivers/ into Http/, Message/ and Plugins/
subfolders. The sync message driver belongs in Message/ alongside the
async one, since it's the same message-queue-style request/response
category, not a plugin. Namespace stays PactNet.Drivers for now since
the rest of the tree hasn't moved yet; that follows once pact-foundation#551 lands.

@YOU54F YOU54F left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @max-lancaster,

This looks really good to me,

Could you add two commits to help get this passing in CI, and an additional to test with the latest ffi and pact protobuf plugin version

  1. add protoc to linux arm64 ci (fails without it) YOU54F@07b0695
  2. update the grpc packages to 2.84.0 (there is a bug in 2.82.x and below that affects linux arm64) YOU54F@a82f8ce
  3. Regenerate the fixtured pact file with the latest pact protobuf version (0.8.0) YOU54F@9274843 additional bringing in the pact-ffi update.

Tested PR in CI here against my fork

I'll add some comments on the packaged grpc extension about where best to house it!

@max-lancaster

Copy link
Copy Markdown
Author

Thanks @YOU54F I will look into addressing your suggestions.

…rotobuf plugin 0.8.0

Brings in YOU54F/pact-net@92748436 as requested in the PR pact-foundation#551 review.

- pact-ffi 0.4.27 -> 0.5.10; a request header configured with multiple
  values is now matched as one joined value, so the tests and the
  upgrading-to-5 doc reflect that
- pact fixtures regenerated (pactRust metadata, merged X-Request header)
- grpc sample pact regenerated with pact-protobuf-plugin 0.8.0
- grpc sample and plugin docs now request plugin 0.8.0

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@max-lancaster

Copy link
Copy Markdown
Author

@YOU54F thanks for the review and for testing this in CI on your fork. All three items are now on this branch:

  1. protoc installed on the linux arm64 CI job: 3de6c44
  2. grpc / protobuf packages updated to 2.84.0: 7887b3e
  3. pact-ffi upgraded to 0.5.10 and all pact fixtures regenerated, with the gRPC sample pact regenerated using pact-protobuf-plugin 0.8.0: 44d88b5. I also bumped the gRPC sample and the plugin docs to request plugin 0.8.0 so they match the regenerated fixture.

Verified locally on Windows against ffi 0.5.10: PactNet.Tests on net8.0 and net462, plus the gRPC and OrdersApi consumer and provider samples, all pass. The sample pacts regenerated locally with plugin 0.8.0 are byte-identical to the fixtures in your commit.

@YOU54F

YOU54F commented Oct 12, 2026

Copy link
Copy Markdown
Member

No worries dude, my pleasure.

CI is failing as the protobuf plugin when not the disk, will auto download with the framework.

I think it may either be a permissions issue, or a timing issue, as reproducing locally, with my local plugins removed.

After a failing test run, the protobuf plugin is correctly installed to disk, and executable.

       Pact verification failed
       
       Verifier Output
       ---------------
       Verification execution failed: Failed to creating plugins directory
       Verifier Logs
       -------------
       2026-10-12T14:22:40.092247Z  INFO ThreadId(17) pact_verifier: Pact file requires plugins, will load those now
       2026-10-12T14:22:40.092521Z  WARN ThreadId(17) pact_plugin_driver::plugin_manager: Could not load plugin manifest from disk, will try auto install it: Plugin protobuf:0.8.0 was not found (in $HOME/.pact/p
      lugins or $PACT_PLUGIN_DIR)
       2026-10-12T14:22:40.093544Z  INFO ThreadId(17) pact_plugin_driver::repository: Fetching index from github
       2026-10-12T14:22:40.208914Z  INFO ThreadId(17) pact_plugin_driver::plugin_manager: Found an entry for the plugin in the plugin index, will try install that
       2026-10-12T14:22:40.208930Z  INFO ThreadId(17) pact_plugin_driver::download: Fetching root document for source source=https://github.com/pact-foundation/pact-protobuf-plugin/releases/tag/v-0.8.0
       2026-10-12T14:22:40.542707Z  INFO ThreadId(17) pact_plugin_driver::plugin_manager: Creating plugin directory plugin_dir=/Users/yousaf.nabi/.pact/plugins/protobuf-0.8.0
    /Users/yousaf.nabi/dev/pact-foundation/pact-net/src/PactNet/Verifier/InteropVerifierProvider.cs(233): error TESTERROR: 
      GrpcGreeter.Tests.GrpcGreeterTests.VerificationSuccessForRunningProvider (1ms): Error Message: PactNet.Exceptions.PactFailureException : Failed to run the verification
      Stack Trace:
         at PactNet.Verifier.InteropVerifierProvider.Execute() in /Users/yousaf.nabi/dev/pact-foundation/pact-net/src/PactNet/Verifier/InteropVerifierProvider.cs:line 233
         at PactNet.Verifier.PactVerifierSource.Verify() in /Users/yousaf.nabi/dev/pact-foundation/pact-net/src/PactNet/Verifier/PactVerifierSource.cs:line 112
         at GrpcGreeter.Tests.GrpcGreeterTests.VerificationSuccessForRunningProvider() in /Users/yousaf.nabi/dev/pact-foundation/pact-net/samples/Grpc/GrpcGreeter.Tests/GrpcGreeterTest.cs:line 42
         at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)
         at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)
        Standard Output Messages:
       Starting verification...
       Pact verification failed
       
       Verifier Output
       ---------------
       Verification execution failed: Was not able to start plugin process for '/Users/yousaf.nabi/.pact/plugins/protobuf-0.8.0/pact-protobuf-plugin' - No such file or directory (os error 2)
       Verifier Logs
       -------------
       2026-10-12T14:22:40.577038Z  INFO ThreadId(17) pact_verifier: Pact file requires plugins, will load those now
       2026-10-12T14:22:40.577525Z  INFO ThreadId(17) pact_plugin_driver::plugin_host: Starting PluginHost gRPC server on 127.0.0.1:49774

We can circumvent by using the pact-cli to pre-install the plugin

      - uses: pact-foundation/pact-cli@main
      - run: pact plugin install --yes protobuf --version 0.8.0

I wonder if this is something we can cater for in the core, or if it needs handling in the client library. Haven't investigated too far.

FYI, the grpc wrapper is over here now https://github.com/pact-foundation/pact-net-extensions

Just trying to get CI all green over there, and then I will update to your latest commit. It is currently one behind

@YOU54F

YOU54F commented Oct 12, 2026 •

Copy link
Copy Markdown
Member

So with your changes, I was able to build out

both tested with a newly created avro plugin, to test out the additional plugin contribution path

With these in place, users should be able to create new plugins for http / async / sync types, with their own transport (excl. http) or content matchers

@adamrodger - I think this is in a good state to re-review.

  1. Considerations required for this ffi bump user facing change chore(ffi): upgrade to pact-ffi 0.5.10 #566, doc change might need an update, especially with ref to the ffi version (a pact-net user probably doesn't care, would be better as the pact-net version containing the change)
  2. Will need to setup nuget keys in the newly created extensions repo, and test/release post merge/release of this PR

happy to help with any merging, releasing.

@YOU54F
YOU54F requested a review from adamrodger October 12, 2026 17:27
@max-lancaster

Copy link
Copy Markdown
Author

@YOU54F it's a timing issue. I had Claude dig through the failed run and the plugin driver source, and this is what it found:

Running dotnet test on the solution starts GrpcGreeterClient.Tests and GrpcGreeter.Tests in parallel (0.35s apart in the failed run), and both need the protobuf plugin. The driver's auto-install creates the plugin directory and writes pact-plugin.json before it downloads the binary, and the PLUGIN_REGISTER lock is in-process only. So the second process sees the manifest, assumes the plugin is installed, and fails to exec a binary that hasn't landed yet. Your Failed to creating plugins directory variant is the same race from the other side: fs::create_dir fails because the other process just created it.

I pushed a fix in c7f45bb: a new build/install-plugins.sh that pre-installs the plugin (platform-aware, including linux aarch64, which the release's own install-plugin.sh doesn't cover) plus a CI step that runs it before the tests. I went with a script rather than pact-cli to keep it in line with the existing download-native-libs.sh. The CI run for that commit is waiting on workflow approval.

Longer term this is probably worth fixing in the driver itself, e.g. write the manifest last or take a file lock on the plugin directory during install.

Replace build/install-plugins.sh with the pact-foundation/pact-cli action
and `pact plugin install`, as suggested in review. The plugin is still
pre-installed before the tests run so parallel test projects do not race
each other auto-downloading it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@max-lancaster

Copy link
Copy Markdown
Author

@YOU54F follow-up: I've switched to your suggested approach in 6e28aef. The shell script is gone and CI now uses the pact-foundation/pact-cli action plus pact plugin install --yes protobuf --version 0.8.0 before the tests. I pinned the action to v0.10.8 rather than @main, as the action's own docs recommend pinning via the ref. Verified the install command locally on Windows against a clean plugin directory.

@YOU54F

YOU54F commented Oct 12, 2026

Copy link
Copy Markdown
Member

Appreciate the investigation dude, I'll get an issue raised in the pact-plugins repo as the fix will need to go into the rust driver here https://github.com/pact-foundation/pact-plugins/tree/main/drivers/rust/driver

fabio-andre-rodrigues added a commit to fabio-andre-rodrigues/pact-net that referenced this pull request Oct 16, 2026
pact-foundation#551 reorganises Drivers/ into Http/, Message/ and Plugins/
subfolders. The sync message driver belongs in Message/ alongside the
async one, since it's the same message-queue-style request/response
category, not a plugin. Namespace stays PactNet.Drivers for now since
the rest of the tree hasn't moved yet; that follows once pact-foundation#551 lands.
fabio-andre-rodrigues added a commit to fabio-andre-rodrigues/pact-net that referenced this pull request Oct 19, 2026
pact-foundation#551 reorganises Drivers/ into Http/, Message/ and Plugins/
subfolders. The sync message driver belongs in Message/ alongside the
async one, since it's the same message-queue-style request/response
category, not a plugin. Namespace stays PactNet.Drivers for now since
the rest of the tree hasn't moved yet; that follows once pact-foundation#551 lands.
MockServerLogs() drains the process-wide FFI log buffer, which other
test classes running in parallel also drain on every Verify, so the
assertion that it is non-empty fails intermittently. The logs are still
written to the test output. Option 1 from the issue as a stop-gap; the
per-server log fix stays open.

Refs pact-foundation#568

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@max-lancaster

Copy link
Copy Markdown
Author

@YOU54F I went with option 1 from #568 and dropped the NotBeEmpty assertion on the mock server logs in 34a1376, the plugin tests have been passing since the pact-cli change - every failure since then was that one flaky test. #568 stays open for the real fix, can you approve the CI run when you get a chance?

@adamrodger can you take another look when you have a minute? The WithContent change you asked for went in back in September

🤖 Claude written, Max approved ✅

@mefellows

Copy link
Copy Markdown
Member

@YOU54F I went with option 1 from #568 and dropped the NotBeEmpty assertion on the mock server logs in 34a1376, the plugin tests have been passing since the pact-cli change - every failure since then was that one flaky test. #568 stays open for the real fix, can you approve the CI run when you get a chance?

@adamrodger can you take another look when you have a minute? The WithContent change you asked for went in back in September

🤖 Claude written, Max approved ✅

I've hit approve on the CI run - thanks Max!

@YOU54F

YOU54F commented Nov 12, 2026

Copy link
Copy Markdown
Member

@YOU54F I went with option 1 from #568 and dropped the NotBeEmpty assertion on the mock server logs in 34a1376, the plugin tests have been passing since the pact-cli change - every failure since then was that one flaky test. #568 stays open for the real fix, can you approve the CI run when you get a chance?

@adamrodger can you take another look when you have a minute? The WithContent change you asked for went in back in September

🤖 Claude written, Max approved ✅

fab thanks dude! I’ve just had the last blocker merged in pact-ref for the 0.5.x ffi upgrade today so will get that released tomorrow. Also adding compatibility suite plugin tests so we can cover these new changes too in pact-net.

@YOU54F

YOU54F commented Nov 13, 2026

Copy link
Copy Markdown
Member

Please feel free to rebase on top of #569

@adamrodger This is good to go, would it be useful if I created an RFC for changes for pact-net 5.1.0?

It would include

I would also like to follow up after that, to stop them waiting too long

These are currently, or all will be supported by examples

Along with a few other QoL fixes provided by contributors, and. some maintainer aids such as coverage in the Pact compatibility suite for plugins pact-foundation/pact-compatibility-suite#18

I appreciate it it will be a-lot to review, but I am confident that everything is well tested, and I am happy to support ongoing concerns, issues, and maintenance if you are time poor at the moment. Will be happy holding the bar of quality high for pact-net.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants