Skip to content

test(auth): shared OAuth-flow conformance suite over fake and durable AuthFlowManager - #6114

Merged
ilblackdragon merged 2 commits into
mainfrom
test/auth-flow-conformance
Jul 17, 2026
Merged

ilblackdragon merged 2 commits into
mainfrom
test/auth-flow-conformance

Conversation

@ilblackdragon

Copy link
Copy Markdown
Member

Summary

Closes the fake↔durable conformance gap in product-auth OAuth flows, found while pinning the #6105 T4 replay arm (PR #6113): the in-memory InMemoryAuthProductServices fake and the durable FilesystemAuthProductServices had disjoint test suites, so any behavioral divergence between them was structurally undetectable.

Investigating the apparent replay divergence showed the two implementations actually agree at every AuthFlowManager method — claim_oauth_callback is replay-idempotent on terminal flows in both, complete_oauth_callback is fail-closed (FlowAlreadyTerminal) in both via the shared prepare_callback_flow. What differed in #6113's observation was the call path (wrapper-level handle_oauth_callback short-circuits at the idempotent claim), not the implementations. But that agreement was a coincidence of two suites: each impl hand-rolls its terminal-idempotency sets and expiry write-back separately from the shared validation helpers, so they could drift with nothing failing.

What's in it

ironclaw_auth::conformance — a shared, observable-behavior state-machine suite over &dyn AuthFlowManager (lives unconditionally in the lib next to fakes.rs, per the crate's "auth contracts and fake services" charter). Cases:

  • happy completion + both replay arms: a replayed claim is idempotent (returns the completed record with the original grant — what the hosted, unauthenticated callback route depends on for duplicated browser redirects), while a replayed complete stays fail-closed and leaves the record untouched;
  • lapsed flow → UnknownOrExpiredFlow and the record is written back terminal Expired;
  • canceled flow → cancel-specific rejection, re-cancel rejected;
  • unknown flow id → UnknownOrExpiredFlow;
  • state-hash mismatch → CrossScopeDenied without burning the flow (the genuine callback still completes).

Both implementations invoke it:

  • fake: crates/ironclaw_auth/tests/auth_product_contract/oauth_flow_contract.rs
  • durable: tests/integration/oauth_connect.rs over the composed OAuthProductAuthTestBundle's flow_manager() (real FilesystemAuthProductServices; the suite drives pre-exchanged outcomes, so no token-exchange egress — that leg keeps its existing coverage in the same file)

Not covered here

Testing

  • cargo test -p ironclaw_auth — 111 passed (incl. the new fake-tier conformance invocation)
  • cargo test --test reborn_integration_oauth_connect — 3 passed (incl. the durable-tier invocation)
  • cargo clippy -p ironclaw_auth --all-targets --all-features and cargo clippy --test reborn_integration_oauth_connect --all-features — clean; cargo fmt clean

🤖 Generated with Claude Code

… AuthFlowManager

The in-memory fake (ironclaw_auth) and the durable
FilesystemAuthProductServices (ironclaw_reborn_composition) previously
had disjoint test suites, so behavioral divergence between them was
structurally undetectable — their agreement was coincidence, not
contract. Found while pinning the #6105 T4 replay arm: the two impls DO
agree today (claim is replay-idempotent on terminal flows, complete is
fail-closed), but each hand-rolls its terminal-idempotency sets and
expiry write-back separately from the shared validation helpers.

Adds ironclaw_auth::conformance — an observable-behavior state-machine
suite over &dyn AuthFlowManager (happy completion + both replay arms,
expiry write-back, cancel, unknown flow, state-hash mismatch not
burning the flow) — invoked from both tiers:

- fake: auth_product_contract/oauth_flow_contract.rs
- durable: tests/integration/oauth_connect.rs over the composed
  OAuthProductAuthTestBundle flow_manager()

The suite drives pre-exchanged outcomes, so no token-exchange egress is
involved; the exchange leg keeps its existing coverage.

Related: #4202 (crash-window callback cleanup, not covered here),
#5617 (same fakes-only failure class in the identity crate).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-6114 July 15, 2026 06:11 Destroyed
@github-actions github-actions Bot added size: L 200-499 changed lines risk: low Changes to docs, tests, or low-risk modules contributor: core 20+ merged PRs labels Jul 15, 2026
@coderabbitai

coderabbitai Bot commented Jul 15, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 9539120f-e5bc-4944-8c14-de3fda0312e0

📥 Commits

Reviewing files that changed from the base of the PR and between 873ca73 and 966e7f3.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock, !**/Cargo.lock
📒 Files selected for processing (7)
  • Cargo.toml
  • crates/ironclaw_auth/Cargo.toml
  • crates/ironclaw_auth/src/lib.rs
  • crates/ironclaw_auth/src/test_support.rs
  • crates/ironclaw_auth/src/test_support/conformance.rs
  • crates/ironclaw_auth/tests/auth_product_contract/oauth_flow_contract.rs
  • tests/integration/oauth_connect.rs

📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes

    • Strengthened OAuth callback handling across in-memory and durable flows.
    • Improved protection against expired, canceled, unknown, replayed, or cross-scope callback attempts.
    • Preserved valid authorization grants when duplicate callbacks are received.
  • Tests

    • Added shared OAuth conformance coverage for callback state transitions.
    • Added integration tests for durable, filesystem-backed OAuth flows.

Walkthrough

Adds a feature-gated OAuth callback conformance harness covering replay, expiry, cancellation, unknown flows, and state-hash fencing. The harness runs against both in-memory and durable flow managers through new integration tests.

Changes

OAuth callback conformance

Layer / File(s) Summary
Shared conformance contract
crates/ironclaw_auth/src/test_support/*, crates/ironclaw_auth/src/lib.rs, crates/ironclaw_auth/Cargo.toml
Adds the gated test_support::conformance API, deterministic fixtures, callback helpers, flow lookup, and the shared async entry point.
Callback lifecycle cases
crates/ironclaw_auth/src/test_support/conformance.rs
Validates completed-flow replay semantics, expired and canceled flows, unknown flow handling, and non-consuming state-hash mismatches.
Flow-manager coverage
crates/ironclaw_auth/tests/auth_product_contract/oauth_flow_contract.rs, tests/integration/oauth_connect.rs, Cargo.toml
Runs the shared suite against in-memory and durable flow managers using test-only feature dependencies.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Conformance as Conformance harness
  participant InMemory as InMemoryAuthProductServices
  participant Durable as Durable flow manager
  participant Flow as OAuth flow record

  Conformance->>InMemory: Run callback conformance cases
  InMemory->>Flow: Claim, complete, cancel, and read flow
  Flow-->>InMemory: Return lifecycle state and errors
  InMemory-->>Conformance: Return conformance results

  Conformance->>Durable: Run callback conformance cases
  Durable->>Flow: Persist and read lifecycle state
  Flow-->>Durable: Return lifecycle state and errors
  Durable-->>Conformance: Return conformance results
Loading

Possibly related PRs

Suggested reviewers: henrypark133, think-in-universe

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Conventional Commits format is used and the title accurately describes the shared OAuth conformance suite change.
Description check ✅ Passed The description covers summary, scope, exclusions, and testing, but omits template sections like Change Type, Linked Issue, and impact checklists.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request introduces a shared OAuth-flow state-machine conformance suite to ensure that both the in-memory fake and durable filesystem implementations of AuthFlowManager satisfy the same behavioral contracts. It integrates this suite into the test suites of both implementations. The feedback suggests increasing the expiration offset in the expired flow test from 1 second to 10 seconds to prevent potential test flakiness on backends with lower timestamp precision.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread crates/ironclaw_auth/src/conformance.rs Outdated
scope,
provider,
tag,
Utc::now() - Duration::seconds(1),

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.

medium

Using a very short expiration offset like Duration::seconds(1) can lead to flaky tests on durable implementations (such as databases or filesystems) that truncate timestamps to second precision, or during extremely fast test execution where the clock hasn't advanced. Increasing this to a larger offset (e.g., Duration::seconds(10)) ensures the flow is reliably treated as expired across all backends.

Suggested change
Utc::now() - Duration::seconds(1),
Utc::now() - Duration::seconds(10),

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Applied in 966e7f3 — widened the expired-flow offset from Duration::seconds(1) to Duration::seconds(10) so second-precision timestamp truncation on the durable FilesystemAuthProductServices backend can't leave the flow borderline-unexpired. Thanks — good catch for the durable tier.

(The file also moved in this push: crates/ironclaw_auth/src/conformance.rs → crates/ironclaw_auth/src/test_support/conformance.rs, now feature-gated so its panics don't ship in production binaries.)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/integration/oauth_connect.rs`:
- Around line 144-163: Update tests/integration/coverage-floor.toml to record
the intentional coverage increase from
durable_flow_manager_satisfies_shared_oauth_flow_conformance and satisfy the
existing tests/integration/**/*.rs coverage-floor requirement. Preserve the
established floor-file format and adjust only the affected integration coverage
entries.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 0c7b7183-2790-4a0f-bc09-01b7a16e5864

📥 Commits

Reviewing files that changed from the base of the PR and between 1a56f28 and 873ca73.

📒 Files selected for processing (4)
  • crates/ironclaw_auth/src/conformance.rs
  • crates/ironclaw_auth/src/lib.rs
  • crates/ironclaw_auth/tests/auth_product_contract/oauth_flow_contract.rs
  • tests/integration/oauth_connect.rs

Comment thread tests/integration/oauth_connect.rs
@github-actions

github-actions Bot commented Jul 15, 2026 •

Copy link
Copy Markdown
Contributor

Coverage ratchet

Ratchet mode: ENFORCING

RATCHET PASS: global
  observed: 85.69% (305795 / 356859 lines)
  floor:    85.3% (tolerance 0.5pp -> effective floor 84.8%)
  denominator: 356859 lines now vs 320188 at floor capture (+36671 lines, +11.45%) — material change (>5%)

⚠️ 2 Reborn crate(s) have 0 int-tier coverage (target: 0) — ironclaw_prompt_envelope, ironclaw_scripts

Reborn integration-tier coverage

Line coverage (Reborn crates): 85.69% — 305795 / 356859 lines

Per-crate breakdown (63 crates, lowest-covered first)
Crate Line % Covered / Total
ironclaw_prompt_envelope 0% 0 / 88
ironclaw_scripts 0% 0 / 345
ironclaw_runtime_policy 31.75% 80 / 252
ironclaw_event_projections 43.31% 673 / 1554
ironclaw_run_state 53.07% 225 / 424
ironclaw_authorization 53.89% 464 / 861
ironclaw_observability 61.54% 16 / 26
ironclaw_webui_v2 62.98% 2684 / 4262
ironclaw_mcp 64.89% 595 / 917
ironclaw_triggers 65.44% 2142 / 3273
ironclaw_reborn_cli 66.18% 4488 / 6781
ironclaw_dispatcher 67.15% 92 / 137
ironclaw_filesystem 67.69% 3932 / 5809
ironclaw_memory 69.2% 773 / 1117
ironclaw_reborn_migration 71.57% 1551 / 2167
ironclaw_trust 72.88% 661 / 907
ironclaw_capabilities 74.39% 1685 / 2265
ironclaw_wasm_limiter 74.6% 47 / 63
ironclaw_reborn_event_store 74.67% 958 / 1283
ironclaw_extractors 74.72% 538 / 720
ironclaw_llm 78.25% 20193 / 25805
ironclaw_product_context 78.57% 11 / 14
ironclaw_first_party_extensions 78.86% 5579 / 7075
ironclaw_process_sandbox 80.65% 671 / 832
ironclaw_wasm_product_adapters 80.71% 1448 / 1794
ironclaw_memory_native 81.22% 3205 / 3946
ironclaw_secrets 82.79% 2794 / 3375
ironclaw_events 82.86% 1765 / 2130
ironclaw_reborn_identity 83.59% 433 / 518
ironclaw_wasm 83.97% 1011 / 1204
ironclaw_reborn_config 84.06% 1814 / 2158
ironclaw_processes 84.44% 993 / 1176
ironclaw_auth 84.86% 3341 / 3937
ironclaw_turns 85.03% 13721 / 16136
ironclaw_host_api 85.2% 2665 / 3128
ironclaw_product_workflow 85.8% 10920 / 12728
ironclaw_projects 85.92% 659 / 767
ironclaw_network 86.12% 670 / 778
ironclaw_common 86.66% 1741 / 2009
ironclaw_slack_v2_adapter 86.79% 1806 / 2081
ironclaw_threads 86.93% 4708 / 5416
ironclaw_product_adapters 87.18% 3265 / 3745
ironclaw_skills 87.58% 4470 / 5104
ironclaw_hooks 87.78% 9921 / 11302
ironclaw_product_adapter_registry 88.06% 531 / 603
ironclaw_reborn_traces 88.19% 11946 / 13546
ironclaw_host_runtime 88.53% 17437 / 19697
ironclaw_extensions 89.33% 2955 / 3308
ironclaw_reborn_composition 89.4% 81309 / 90954
ironclaw_approvals 89.41% 1587 / 1775
ironclaw_runner 89.5% 16990 / 18983
ironclaw_reborn_openai_compat 89.5% 3778 / 4221
ironclaw_conversations 90.39% 3123 / 3455
ironclaw_event_streams 90.82% 1009 / 1111
ironclaw_loop_host 92.24% 15043 / 16308
ironclaw_resources 92.69% 5134 / 5539
ironclaw_attachments 93.06% 630 / 677
ironclaw_reborn_webui_ingress 93.19% 2217 / 2379
ironclaw_telegram_v2_adapter 93.91% 2592 / 2760
ironclaw_agent_loop 94.81% 9201 / 9705
ironclaw_safety 95.04% 3677 / 3869
ironclaw_outbound 95.59% 3556 / 3720
ironclaw_first_party_extension_ports 95.62% 3672 / 3840

This table itself is informational and never gates the PR on its own — not the percentage, not the per-crate holes, not the 0-coverage callout. A separate coverage ratchet (dry-run until enforce=true; see tests/integration/coverage-floor.toml) can fail the build on specific configured floors.

Exemptions (3 entry/entries excluded from the accounting above)
Module / Crate Reason Issue
crate: ironclaw_embeddings v1-only: consumed only by root ironclaw (src/app.rs, src/tools/builtin/memory.rs, src/workspace/mod.rs, src/config/{mod,embeddings}.rs); no crates/* dependents. Covered by "Tests (Legacy)". #5657
crate: ironclaw_gateway v1-only: consumed only by root ironclaw (src/channels/web/platform/static_files.rs, src/channels/web/handlers/frontend.rs); no crates/* dependents. Covered by "Tests (Legacy)". #5657
crate: ironclaw_tui v1-only: consumed only by root ironclaw (src/main.rs, src/channels/tui.rs); no crates/* dependents. Crate's own doc comment confirms it bridges INTO v1, not Reborn. Covered by "Tests (Legacy)". #5657

@railway-app

railway-app Bot commented Jul 15, 2026 •

Copy link
Copy Markdown

🚅 Deployed to the ironclaw-pr-6114 environment in ironclaw-ci-preview

Service Status Web Updated (UTC)
ironclaw ✅ Success (View Logs) Web Jul 16, 2026 at 9:24 pm

…anics gate (#6114 CI)

The shared OAuth-flow conformance suite landed as an ungated `pub mod
conformance;`, so its `.expect()`/`assert!`/`panic!` calls compiled into
production binaries and the "No panics in production code" CI gate failed
(cascading into the aggregate "Code Style" check). It was modeled on
`fakes.rs` living "unconditionally in the lib" — but `fakes.rs` is
panic-free (returns `Result`); a panic-on-violation assertion harness must
not ship in production.

Move it under a feature-gated `test_support` module — the repo's sanctioned
pattern (ironclaw_agent_loop, ironclaw_product_adapters, …) and exactly why
`check_no_panics.py` path-exempts `src/test_support/**` ("ships zero bytes in
production"):

- relocate `src/conformance.rs` -> `src/test_support/conformance.rs`, gated
  behind `#[cfg(any(test, feature = "test-support"))]`;
- add the `test-support` feature + a self dev-dependency so the crate's own
  `tests/` reach it, and a root `[dev-dependencies]` `ironclaw_auth` with the
  feature so `reborn_integration_oauth_connect` reaches it (tests only —
  release binaries stay clean under resolver 2);
- update both callers to `ironclaw_auth::test_support::conformance::…`.

Also address the review nit: widen the expired-flow offset from 1s to 10s so
second-precision timestamp truncation on the durable backend can't flake
(gemini-code-assist).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-6114 July 16, 2026 21:08 Destroyed
@github-actions github-actions Bot added scope: dependencies Dependency updates risk: medium Business logic, config, or moderate-risk modules and removed risk: low Changes to docs, tests, or low-risk modules labels Jul 16, 2026
@ilblackdragon

Copy link
Copy Markdown
Member Author

Pushed 966e7f310 to fix CI.

Root cause: the new conformance suite landed as an ungated pub mod conformance;, so its .expect()/assert!/panic! calls compiled into production binaries — the No panics in production code gate failed, cascading into the aggregate Code Style (fmt + clippy) check (the only other red). It was modeled on fakes.rs living "unconditionally in the lib," but fakes.rs is panic-free (returns Result); a panic-on-violation assertion harness must not ship in production.

Fix: moved it under a feature-gated test_support module — the repo's sanctioned pattern (ironclaw_agent_loop, ironclaw_product_adapters, …) and exactly why check_no_panics.py path-exempts src/test_support/** ("ships zero bytes in production"):

  • src/conformance.rs → src/test_support/conformance.rs, gated behind #[cfg(any(test, feature = "test-support"))];
  • added the test-support feature + a self dev-dependency (crate's own tests/) and a root [dev-dependencies] ironclaw_auth with the feature (reborn_integration_oauth_connect) — tests only; release binaries stay clean under resolver 2;
  • both callers now use ironclaw_auth::test_support::conformance::….

Also widened the expired-flow offset 1s→10s (gemini nit). Verified locally: check_no_panics.py passes, cargo clippy -p ironclaw_auth --all-targets --all-features -D warnings clean, feature-off production build clean, and both conformance tests (fake + durable integration) pass. See inline replies for the two review comments.

This branch was successfully deployed

No deployments
ironclaw-ci-preview / ironclaw-pr-6114 — 966e7f31 Deployed Jul 16, 2026 by railway-app[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contributor: core 20+ merged PRs risk: medium Business logic, config, or moderate-risk modules scope: dependencies Dependency updates size: L 200-499 changed lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant