Skip to content

Tests: centralize contract coverage follow-ups#48751

Merged
vincentkoc merged 10 commits intoopenclaw:mainfrom
vincentkoc:vincentkoc-code/slack-plugin-interactive-dedupe
Mar 17, 2026
Merged

Tests: centralize contract coverage follow-ups#48751
vincentkoc merged 10 commits intoopenclaw:mainfrom
vincentkoc:vincentkoc-code/slack-plugin-interactive-dedupe

Conversation

@vincentkoc
Copy link
Copy Markdown
Member

Summary

  • Problem: the global contract system still had follow-up gaps around inbound coverage centralization, stricter registry coverage, and contributor-facing contract lanes.
  • Why it matters: contract ownership was partly split between core and older local helpers, and the provider auth-choice contract still overstated what it verified.
  • What changed: centralized inbound contract helpers under the core contract tree, tightened provider/channel contract registry coverage, split plugin/channel contract lanes, and documented the contributor workflow in CONTRIBUTING.md.
  • What did NOT change (scope boundary): no production runtime/plugin behavior changed outside the contract and test harness layer.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

User-visible / Behavior Changes

  • Contributors now have explicit pnpm test:contracts, pnpm test:contracts:channels, and pnpm test:contracts:plugins lanes documented in CONTRIBUTING.md.
  • Extension/channel contract coverage is more centralized under the global contract tree.

Security Impact (required)

  • New permissions/capabilities? No
  • Secrets/tokens handling changed? No
  • New/changed network calls? No
  • Command/tool execution surface changed? No
  • Data access scope changed? No
  • If any Yes, explain risk + mitigation:

Repro + Verification

Environment

  • OS: macOS
  • Runtime/container: local workspace
  • Model/provider: N/A
  • Integration/channel (if any): plugin and channel contract test surfaces
  • Relevant config (redacted): default local dev config

Steps

  1. Rebase the branch onto latest origin/main.
  2. Resolve contract coverage conflicts in the provider/channel contract files.
  3. Run focused static verification on the touched contract files.

Expected

  • The branch rebases cleanly onto current main.
  • Global contract coverage stays centralized and stricter than the older branch state.

Actual

  • The branch now rebases onto current main and keeps the stricter provider fallback and contract-registry assertions.
  • Focused oxlint passes on the touched contract files.

Evidence

Attach at least one:

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)

Human Verification (required)

What you personally verified (not just CI), and how:

  • Verified scenarios: rebased onto current origin/main; resolved the provider auth-choice fallback contract and contract registry conflicts; ran pnpm exec oxlint on the touched contract files.
  • Edge cases checked: confirmed the old cherry-picked provider attribution commits were already superseded by current main and dropped them during rebase.
  • What you did not verify: the full Vitest contract lanes did not complete in this checkout and stayed at runner startup, so I do not have a completed pnpm test:contracts run from this branch.

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

If a bot review conversation is addressed by this PR, resolve that conversation yourself. Do not leave bot review conversation cleanup for maintainers.

Compatibility / Migration

  • Backward compatible? Yes
  • Config/env changes? No
  • Migration needed? No
  • If yes, exact upgrade steps:

Failure Recovery (if this breaks)

  • How to disable/revert this change quickly: revert this PR.
  • Files/config to restore: the global contract registry and inbound contract helper follow-up files under src/channels/plugins/contracts/ and src/plugins/contracts/.
  • Known bad symptoms reviewers should watch for: contract suites failing to discover bundled plugin coverage correctly, or inbound contract helpers no longer matching the global registry ownership model.

Risks and Mitigations

  • Risk:
    • The tightened registry assertions could expose additional missing coverage in CI after merge.
    • Mitigation: this is confined to the contract/test layer and is straightforward to adjust without runtime rollback.

@openclaw-barnacle openclaw-barnacle bot added channel: discord Channel integration: discord channel: signal Channel integration: signal channel: whatsapp-web Channel integration: whatsapp-web labels Mar 17, 2026
@vincentkoc vincentkoc self-assigned this Mar 17, 2026
@openclaw-barnacle openclaw-barnacle bot added size: XL maintainer Maintainer-authored PR labels Mar 17, 2026
@vincentkoc vincentkoc marked this pull request as ready for review March 17, 2026 05:26
@vincentkoc vincentkoc merged commit 6c866b8 into openclaw:main Mar 17, 2026
27 of 39 checks passed
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 17, 2026

Greptile Summary

This PR centralizes the contract test infrastructure for both channel plugins and provider plugins — moving helper files from test/helpers/ into src/channels/plugins/contracts/, consolidating five per-channel inbound contract tests into a single inbound.contract.test.ts, deriving pluginContractRegistry and surfaceContractRegistry dynamically from bundledChannelPlugins instead of hand-maintaining per-plugin lists, adding a sessionBindingContractRegistry with Discord/Feishu/Telegram entries, promoting matrix's directory contract from "presence only" to "lookups" with a proper test config, and extracting shared helpers (uniqueProviderContractProviders, providerContractPluginIds, requireProviderContractProvider) to reduce duplication across plugin contract tests.

Key issues found:

  • Competing vi.mock() conflict in inbound.contract.test.ts (src/channels/plugins/contracts/inbound.contract.test.ts, lines 189–197): The file both imports inboundCtxCapture from inbound-contract-dispatch-mock.ts (which registers its own vi.mock for auto-reply/dispatch.js capturing to inboundCtxCapture.ctx) and also directly registers a second vi.mock for the same resolved module that captures into signalCapture.ctx. Under Vitest's hoisting rules only one mock can win; if the test-file's local mock wins, the Discord test (line 341) will always fail since it asserts inboundCtxCapture.ctx – a variable the winning mock never populates. The PR author explicitly noted the full contract test lane did not complete during this checkout.
  • Discord test checks the wrong capture variable (line 341): Consequent to the above, the Discord test should use signalCapture.ctx (fed by the local dispatch mock) rather than inboundCtxCapture.ctx.
  • Auth-choice contract coverage narrowed (src/plugins/contracts/auth-choice.contract.test.ts): Specific concrete auth-choice string → provider mappings (e.g. "minimax-global-oauth" → "minimax-portal") are replaced by a dynamically-constructed round-trip, which is intentional per the PR description but removes pinned regression coverage for those strings.

Confidence Score: 2/5

  • Not safe to merge as-is: the consolidated inbound contract test contains a competing vi.mock() conflict that will likely cause the Discord inbound test to always fail, and the author confirmed the contract lanes did not complete locally.
  • The registry refactors and session-binding centralization are solid improvements. However, the new inbound.contract.test.ts introduces a structural mock conflict — both an imported helper and the test file itself register vi.mock for the same auto-reply/dispatch.js module with different capture targets — which means the Discord inbound test will very likely fail in CI. The PR description explicitly acknowledges the contract suite did not complete in this checkout, which increases the risk. The auth-choice contract weakening is deliberate and lower risk, but adds to the overall concern that the test coverage claims of this PR have not been fully verified.
  • src/channels/plugins/contracts/inbound.contract.test.ts requires the most attention due to the dual-mock conflict and the mismatched capture variable in the Discord test case.

Comments Outside Diff (1)

  1. src/channels/plugins/contracts/inbound.contract.test.ts, line 341-352 (link)

    P1 Discord test checks the wrong capture variable

    Because of the dual-mock conflict noted above, inboundCtxCapture will only be populated if the mock from inbound-contract-dispatch-mock.js is the active one. The direct vi.mock in this file (lines 189-197) captures dispatch calls into signalCapture, not inboundCtxCapture. If the local mock wins, this test will always see inboundCtxCapture.ctx === undefined and fail.

    For consistency with how Signal, Telegram, and WhatsApp are captured in this same file, the Discord test should use signalCapture.ctx instead:

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/channels/plugins/contracts/inbound.contract.test.ts
    Line: 341-352
    
    Comment:
    **Discord test checks the wrong capture variable**
    
    Because of the dual-mock conflict noted above, `inboundCtxCapture` will only be populated if the mock from `inbound-contract-dispatch-mock.js` is the active one. The direct `vi.mock` in this file (lines 189-197) captures dispatch calls into `signalCapture`, not `inboundCtxCapture`. If the local mock wins, this test will always see `inboundCtxCapture.ctx === undefined` and fail.
    
    For consistency with how Signal, Telegram, and WhatsApp are captured in this same file, the Discord test should use `signalCapture.ctx` instead:
    
    
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/channels/plugins/contracts/inbound.contract.test.ts
Line: 189-197

Comment:
**Competing `vi.mock()` for the same dispatch module**

This test file imports `inboundCtxCapture` from `./inbound-contract-dispatch-mock.js` (line 169), which itself registers `vi.mock("../../../auto-reply/dispatch.js", ...)` that captures context into `inboundCtxCapture.ctx`. The same file then registers a second, conflicting `vi.mock()` for the same resolved path (`src/auto-reply/dispatch.js`) here, which captures into `signalCapture.ctx` instead.

Only one mock can be active at a time. If the test file's direct mock wins (which is likely since Vitest hoists `vi.mock` before imports are resolved), then:
- The Discord test at line 341 – which asserts `expect(inboundCtxCapture.ctx).toBeTruthy()` – will always fail because `inboundCtxCapture.ctx` is never populated by the winning mock.
- The Signal test (line 354) will pass, while the Discord test silently catches a wrong invariant.

The PR description acknowledges "the full Vitest contract lanes did not complete in this checkout", so this conflict may not have been caught locally.

One way to resolve this would be to drop the import of `inboundCtxCapture`/the helper's `vi.mock` and instead update the Discord test to use `signalCapture.ctx` (populated by the local mock), or vice versa – consolidate all channels onto a single capture variable fed by the single local `vi.mock`.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/channels/plugins/contracts/inbound.contract.test.ts
Line: 341-352

Comment:
**Discord test checks the wrong capture variable**

Because of the dual-mock conflict noted above, `inboundCtxCapture` will only be populated if the mock from `inbound-contract-dispatch-mock.js` is the active one. The direct `vi.mock` in this file (lines 189-197) captures dispatch calls into `signalCapture`, not `inboundCtxCapture`. If the local mock wins, this test will always see `inboundCtxCapture.ctx === undefined` and fail.

For consistency with how Signal, Telegram, and WhatsApp are captured in this same file, the Discord test should use `signalCapture.ctx` instead:

```suggestion
  it("keeps Discord inbound context finalized", async () => {
    const messageCtx = await createBaseDiscordMessageContext({
      cfg: { messages: {} },
      ackReactionScope: "direct",
      ...createDiscordDirectMessageContextOverrides(),
    });

    await processDiscordMessage(messageCtx);

    expect(signalCapture.ctx).toBeTruthy();
    expectChannelInboundContextContract(signalCapture.ctx!);
  });
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/plugins/contracts/auth-choice.contract.test.ts
Line: 121-148

Comment:
**Auth-choice test now only verifies round-trip, not concrete string mapping**

The old test explicitly verified that specific auth-choice strings route to the correct provider, e.g.:

```ts
{ authChoice: "minimax-global-oauth" as const, expectedProvider: "minimax-portal" },
```

The new test constructs auth choices dynamically via `buildProviderPluginMethodChoice(provider.id, provider.auth[0]?.id ?? "default")`, which verifies only that a round-trip through the provider's own first auth method resolves back to itself. This no longer catches regressions where a concrete auth-choice string (like `"minimax-global-oauth"`) silently stops resolving to the expected provider (`"minimax-portal"`).

If the intent is to remove the over-specification of individual auth-choice strings (as the PR description suggests), this is acceptable as a deliberate scope reduction. However, it would be worth a comment in the test acknowledging the narrowed contract scope so future contributors understand why the concrete string mapping is no longer asserted.

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: 7749375

Comment on lines +189 to +197

await processDiscordMessage(messageCtx);

expect(inboundCtxCapture.ctx).toBeTruthy();
expectChannelInboundContextContract(inboundCtxCapture.ctx!);
});

it("keeps Signal inbound context finalized", async () => {
const handler = createSignalEventHandler(
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.

P1 Competing vi.mock() for the same dispatch module

This test file imports inboundCtxCapture from ./inbound-contract-dispatch-mock.js (line 169), which itself registers vi.mock("../../../auto-reply/dispatch.js", ...) that captures context into inboundCtxCapture.ctx. The same file then registers a second, conflicting vi.mock() for the same resolved path (src/auto-reply/dispatch.js) here, which captures into signalCapture.ctx instead.

Only one mock can be active at a time. If the test file's direct mock wins (which is likely since Vitest hoists vi.mock before imports are resolved), then:

  • The Discord test at line 341 – which asserts expect(inboundCtxCapture.ctx).toBeTruthy() – will always fail because inboundCtxCapture.ctx is never populated by the winning mock.
  • The Signal test (line 354) will pass, while the Discord test silently catches a wrong invariant.

The PR description acknowledges "the full Vitest contract lanes did not complete in this checkout", so this conflict may not have been caught locally.

One way to resolve this would be to drop the import of inboundCtxCapture/the helper's vi.mock and instead update the Discord test to use signalCapture.ctx (populated by the local mock), or vice versa – consolidate all channels onto a single capture variable fed by the single local vi.mock.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/channels/plugins/contracts/inbound.contract.test.ts
Line: 189-197

Comment:
**Competing `vi.mock()` for the same dispatch module**

This test file imports `inboundCtxCapture` from `./inbound-contract-dispatch-mock.js` (line 169), which itself registers `vi.mock("../../../auto-reply/dispatch.js", ...)` that captures context into `inboundCtxCapture.ctx`. The same file then registers a second, conflicting `vi.mock()` for the same resolved path (`src/auto-reply/dispatch.js`) here, which captures into `signalCapture.ctx` instead.

Only one mock can be active at a time. If the test file's direct mock wins (which is likely since Vitest hoists `vi.mock` before imports are resolved), then:
- The Discord test at line 341 – which asserts `expect(inboundCtxCapture.ctx).toBeTruthy()` – will always fail because `inboundCtxCapture.ctx` is never populated by the winning mock.
- The Signal test (line 354) will pass, while the Discord test silently catches a wrong invariant.

The PR description acknowledges "the full Vitest contract lanes did not complete in this checkout", so this conflict may not have been caught locally.

One way to resolve this would be to drop the import of `inboundCtxCapture`/the helper's `vi.mock` and instead update the Discord test to use `signalCapture.ctx` (populated by the local mock), or vice versa – consolidate all channels onto a single capture variable fed by the single local `vi.mock`.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +121 to +148
it("maps provider-plugin choices through the shared preferred-provider fallback resolver", async () => {
const pluginFallbackScenarios = [
"github-copilot",
"qwen-portal",
"minimax-portal",
"modelstudio",
"ollama",
].map((providerId) => {
const provider = requireProviderContractProvider(providerId);
return {
authChoice: buildProviderPluginMethodChoice(provider.id, provider.auth[0]?.id ?? "default"),
expectedProvider: provider.id,
};
});

for (const scenario of scenarios) {
for (const scenario of pluginFallbackScenarios) {
resolvePreferredProviderPluginProvidersMock.mockClear();
await expect(
resolvePreferredProviderForAuthChoice({ choice: scenario.authChoice }),
resolvePreferredProviderForAuthChoice({ choice: scenario.authChoice as AuthChoice }),
).resolves.toBe(scenario.expectedProvider);
expect(resolvePreferredProviderPluginProvidersMock).toHaveBeenCalled();
}

resolvePreferredProviderPluginProvidersMock.mockClear();
await expect(
resolvePreferredProviderForAuthChoice({ choice: "unknown" as AuthChoice }),
).resolves.toBe(undefined);
expect(resolvePreferredProviderPluginProvidersMock).toHaveBeenCalled();
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.

P2 Auth-choice test now only verifies round-trip, not concrete string mapping

The old test explicitly verified that specific auth-choice strings route to the correct provider, e.g.:

{ authChoice: "minimax-global-oauth" as const, expectedProvider: "minimax-portal" },

The new test constructs auth choices dynamically via buildProviderPluginMethodChoice(provider.id, provider.auth[0]?.id ?? "default"), which verifies only that a round-trip through the provider's own first auth method resolves back to itself. This no longer catches regressions where a concrete auth-choice string (like "minimax-global-oauth") silently stops resolving to the expected provider ("minimax-portal").

If the intent is to remove the over-specification of individual auth-choice strings (as the PR description suggests), this is acceptable as a deliberate scope reduction. However, it would be worth a comment in the test acknowledging the narrowed contract scope so future contributors understand why the concrete string mapping is no longer asserted.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/plugins/contracts/auth-choice.contract.test.ts
Line: 121-148

Comment:
**Auth-choice test now only verifies round-trip, not concrete string mapping**

The old test explicitly verified that specific auth-choice strings route to the correct provider, e.g.:

```ts
{ authChoice: "minimax-global-oauth" as const, expectedProvider: "minimax-portal" },
```

The new test constructs auth choices dynamically via `buildProviderPluginMethodChoice(provider.id, provider.auth[0]?.id ?? "default")`, which verifies only that a round-trip through the provider's own first auth method resolves back to itself. This no longer catches regressions where a concrete auth-choice string (like `"minimax-global-oauth"`) silently stops resolving to the expected provider (`"minimax-portal"`).

If the intent is to remove the over-specification of individual auth-choice strings (as the PR description suggests), this is acceptable as a deliberate scope reduction. However, it would be worth a comment in the test acknowledging the narrowed contract scope so future contributors understand why the concrete string mapping is no longer asserted.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 77493753a4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +31 to +35
vi.mock("../../../auto-reply/dispatch.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../../auto-reply/dispatch.js")>();
return {
...actual,
dispatchInboundMessage: dispatchInboundMessageMock,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep a single dispatch mock for inbound context capture

This file imports inboundCtxCapture from inbound-contract-dispatch-mock, which already mocks src/auto-reply/dispatch, but then immediately remocks the same module here and routes all dispatch calls only through dispatchInboundMessageMock. In the Discord contract case, processDiscordMessage uses that remocked dispatch path, so inboundCtxCapture.ctx is not populated and the Discord assertion becomes order-dependent or fails outright. Use one shared mock (or set both capture sinks in this mock) so Discord and Signal captures stay deterministic.

Useful? React with 👍 / 👎.

nikolaisid pushed a commit to nikolaisid/openclaw that referenced this pull request Mar 18, 2026
* Plugins: harden global contract coverage

* Channels: tighten global contract coverage

* Channels: centralize inbound contract coverage

* Channels: move inbound contract helpers into core

* Tests: rename local inbound context checks

* Tests: stabilize contract runner profile

* Tests: split scoped contract lanes

* Channels: move inbound dispatch testkit into contracts

* Plugins: share provider contract registry helpers

* Plugins: reuse provider contract registry helpers
sbezludny pushed a commit to sbezludny/openclaw that referenced this pull request Mar 27, 2026
* Plugins: harden global contract coverage

* Channels: tighten global contract coverage

* Channels: centralize inbound contract coverage

* Channels: move inbound contract helpers into core

* Tests: rename local inbound context checks

* Tests: stabilize contract runner profile

* Tests: split scoped contract lanes

* Channels: move inbound dispatch testkit into contracts

* Plugins: share provider contract registry helpers

* Plugins: reuse provider contract registry helpers
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: discord Channel integration: discord channel: signal Channel integration: signal channel: whatsapp-web Channel integration: whatsapp-web maintainer Maintainer-authored PR size: XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant