Skip to content

Validate simulate srcAddress against wildcard registrations - #1357

Merged
DZakh merged 5 commits into
mainfrom
claude/hopeful-cannon-0243na
Jun 29, 2026
Merged

DZakh merged 5 commits into
mainfrom
claude/hopeful-cannon-0243na

Conversation

@DZakh

@DZakh DZakh commented Jun 29, 2026 •

Copy link
Copy Markdown
Member

Summary

Move srcAddress validation for simulate items from the main thread (before workers start) to the worker thread (after handler registrations are applied). This allows the validation to correctly recognize events registered as wildcards via indexer.onEvent({ wildcard: true }), which are invisible to the main thread's config.

Key Changes

  • TestIndexer.res: Removed upfront srcAddress validation before worker startup. Validation now runs in patchConfig after registrations are applied, where config.isWildcard reflects handler-level wildcard: true declarations.

  • Main.res: Introduced runUntilFatalError promise to capture fatal errors (including validation failures) and reject it, allowing callers to handle setup and runtime failures uniformly. The test worker now receives validation errors as rejected promises instead of process exits.

  • TestIndexerWorker: Enhanced error handling to forward validation failures from patchConfig to the parent thread via the worker error event, using setImmediate to ensure uncaught exceptions surface properly.

  • SimulateItems.res: Added clarifying comment that config must have registrations applied before validation, since wildcard status depends on handler-level declarations.

  • EventHandler.test.ts: Updated test to use assert.rejects instead of assert.throws, reflecting that validation now runs asynchronously in the worker.

  • WildcardSimulate_test.res: New test file with two cases:

    • Validates that a wildcard-registered event with a concrete srcAddress doesn't fail validation
    • Confirms validation still fires for genuinely non-indexed, non-wildcard events

Implementation Details

The core issue: config.yaml doesn't capture indexer.onEvent({ wildcard: true }), so the main thread can't distinguish wildcard from non-wildcard events. Moving validation into the worker (via patchConfig) runs after registrations are applied, giving access to the true wildcard status. Fatal errors from patchConfig now flow through Main.start's rejection path, which the test worker catches and forwards to the parent thread.

https://claude.ai/code/session_0175WwoErWZNsba95RnrxRyb

Summary by CodeRabbit

  • Bug Fixes
    • Reworked fatal-error handling so failures reliably propagate and surface consistently.
    • Fixed simulate srcAddress validation to reflect worker-applied handler registrations, including correct wildcard vs non-wildcard behavior and clearer rejection messaging.
  • Tests
    • Added scenario suite covering wildcard vs non-wildcard simulate routing behavior.
    • Added dynamic address simulation tests, including the documented limitation for same-run dependencies.
    • Updated existing tests to expect async/promise rejections.
  • Documentation
    • Added clarifying comments about simulate srcAddress validation and wildcard configuration interpretation.

SimulateItems.validateSrcAddresses ran in the main thread against the
config from Config.loadWithoutRegistrations(), whose isWildcard reflects
config.yaml only. Events made wildcard via indexer.onEvent({ wildcard:
true }) read back as non-wildcard there, so createTestIndexer().process()
threw "resolved to address ... which isn't indexed" for a no-address
contract whose events are wildcard-handled. The main thread can't know
handler-level wildcard flags: registrations are applied inside the worker
and a real user never loads/flushes them in the test thread.

Move the srcAddress validation into the worker's patchConfig, where the
config already has registrations applied (so isWildcard is correct) and
the per-chain indexing addresses are available from initialState (matched
by chainId).

Make Main.start reject its promise on any fatal error instead of handling
the consequence inline: onError rejects a deferred (runUntilFatalError)
that start awaits, for both test and non-test runs. The caller logs and
decides what to do — Bin.res's catch logs the cause and exits with a
failure code; the test worker forwards the rejection to the parent via the
worker error event. This unifies setup and runtime failures onto one
channel (a rejected Main.start promise), drops the test-only setImmediate
from Main.start, and leaves logging to the caller so a fatal error is
logged once. A clean run leaves the deferred pending; the process still
exits via ExitOnCaughtUp (success) or naturally when the loop drains.

The pre-flight check was synchronous, so the existing "isn't indexed" test
now awaits a rejected promise.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175WwoErWZNsba95RnrxRyb
@coderabbitai

coderabbitai Bot commented Jun 29, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 14 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 28ea88fb-0b4d-48ed-9852-effb4b469df7

📥 Commits

Reviewing files that changed from the base of the PR and between 9593d60 and ff92932.

📒 Files selected for processing (3)
  • packages/envio/src/Bin.res
  • packages/envio/src/Main.res
  • packages/envio/src/TestIndexer.res
📝 Walkthrough

Walkthrough

Main.start now waits on a deferred fatal-error promise. Simulate srcAddress validation moved into worker patchConfig after handler registration, and tests now cover async rejection, wildcard routing, and dynamic address registration.

Changes

Wildcard simulate validation fix

Layer / File(s) Summary
Main.start fatal error promise
packages/envio/src/Main.res
Introduces runUntilFatalError, updates onError to reject it with a prettified exception string, and awaits that promise after IndexerLoop.start.
Worker-side simulate validation
packages/envio/src/TestIndexer.res, packages/envio/src/SimulateItems.res
Moves simulate srcAddress validation into worker patchConfig after handler registrations are applied, removes the main-thread preflight check, and adds comments about validation timing and wildcard handler routing.
Simulate validation tests
scenarios/test_codegen/test/EventHandler.test.ts, scenarios/test_codegen/test/WildcardSimulate_test.res, scenarios/test_codegen/test/SimulateDynamicAddress_test.res
Updates the event-handler test to expect a rejected promise and adds wildcard and dynamic-address scenario tests for simulate srcAddress handling.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly captures the main change: simulate srcAddress validation now accounts for wildcard registrations.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.

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

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🤖 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 `@packages/envio/src/TestIndexer.res`:
- Around line 878-887: The preflight in TestIndexer.validateSrcAddresses is
using only initialState.chains indexingAddresses, so it misses addresses
registered by earlier simulated items in the same batch. Update the
simulate-batch validation flow so address checks happen incrementally as items
are applied, or otherwise use the evolving per-chain indexingAddresses state
before validating each simulated item. Keep the fix localized around
SimulateItems.validateSrcAddresses and the logic that reads from
initialState.chains so later items can see registrations produced earlier in the
run.

In `@scenarios/test_codegen/test/WildcardSimulate_test.res`:
- Line 36: The wildcard simulation tests are using t.expect on only
result.changes.length, which does not verify the full returned value and
violates the test guidelines. Update the affected assertions in
WildcardSimulate_test.res to use Assert directly on the returned value from the
wildcard simulation helpers, and replace the field-level checks in both test
cases with a single whole-value assertion so the expected result is validated
end-to-end.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5e9a0d9c-fc86-40f4-bea1-bfef6efae800

📥 Commits

Reviewing files that changed from the base of the PR and between 7a6d708 and 974005e.

📒 Files selected for processing (5)
  • packages/envio/src/Main.res
  • packages/envio/src/SimulateItems.res
  • packages/envio/src/TestIndexer.res
  • scenarios/test_codegen/test/EventHandler.test.ts
  • scenarios/test_codegen/test/WildcardSimulate_test.res

Comment on lines +878 to +887
let indexingAddresses =
initialState.chains
->Array.find(c => c.id == chainId)
->Option.mapOr([], c => c.indexingAddresses)
SimulateItems.validateSrcAddresses(
~simulateItems,
~config,
~chainConfig,
~indexingAddresses,
)

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.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

This still rejects simulate batches that register an address earlier in the same run.

indexingAddresses comes from initialState.chains, which is frozen before any simulated item executes. If an earlier simulated item/handler registers a new address and a later simulated event uses that address, this preflight will fail even though the later event would be indexable once the earlier item runs. Consider validating incrementally as simulated items are applied, or deferring this check until it can see registrations produced by prior simulated items in the batch.

🤖 Prompt for 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.

In `@packages/envio/src/TestIndexer.res` around lines 878 - 887, The preflight in
TestIndexer.validateSrcAddresses is using only initialState.chains
indexingAddresses, so it misses addresses registered by earlier simulated items
in the same batch. Update the simulate-batch validation flow so address checks
happen incrementally as items are applied, or otherwise use the evolving
per-chain indexingAddresses state before validating each simulated item. Keep
the fix localized around SimulateItems.validateSrcAddresses and the logic that
reads from initialState.chains so later items can see registrations produced
earlier in the run.


// The handler ran in the worker, producing a checkpoint change instead of
// failing validation.
t.expect(result.changes->Array.length).toEqual(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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use Assert and assert the returned value directly.

Line 36 only checks changes.length, so the first case can pass without proving the wildcard path produced the expected result, and both tests use t.expect instead of the required Assert module. Assert the whole value with Assert here.

As per coding guidelines, **/*_test.res: “Always use single assert to check the whole value instead of multiple asserts for every field” and “In tests, never log — use Assert module for all verifications.”

Also applies to: 68-72

🤖 Prompt for 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.

In `@scenarios/test_codegen/test/WildcardSimulate_test.res` at line 36, The
wildcard simulation tests are using t.expect on only result.changes.length,
which does not verify the full returned value and violates the test guidelines.
Update the affected assertions in WildcardSimulate_test.res to use Assert
directly on the returned value from the wildcard simulation helpers, and replace
the field-level checks in both test cases with a single whole-value assertion so
the expected result is validated end-to-end.

Source: Coding guidelines

@coderabbitai coderabbitai 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.

🧹 Nitpick comments (1)
scenarios/test_codegen/test/SimulateDynamicAddress_test.res (1)

27-27: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use concrete input types in these Utils.magic casts.

These casts still hide the source side behind 'a / _, so they do not satisfy the repo rule for explicit Utils.magic annotations. Please introduce concrete raw payload types for the process config and inline simulate items before coercing them. As per coding guidelines, "When using Utils.magic for type casting in ReScript, always add explicit type annotations: value->(Utils.magic: inputType => outputType)".

Also applies to: 47-47, 83-89, 93-93

🤖 Prompt for 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.

In `@scenarios/test_codegen/test/SimulateDynamicAddress_test.res` at line 27, The
SimulateDynamicAddress_test.res `Utils.magic` casts still use hidden source
types like `'a` and `_`, so update each affected cast to name the concrete raw
input type before coercing to the target type. Use explicit annotations in the
cast sites around the process config and inline simulate items, and keep the
target types such as `Indexer.testIndexerProcessConfig` and the relevant
simulate item types unchanged while making the source side concrete.

Source: Coding guidelines

🤖 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.

Nitpick comments:
In `@scenarios/test_codegen/test/SimulateDynamicAddress_test.res`:
- Line 27: The SimulateDynamicAddress_test.res `Utils.magic` casts still use
hidden source types like `'a` and `_`, so update each affected cast to name the
concrete raw input type before coercing to the target type. Use explicit
annotations in the cast sites around the process config and inline simulate
items, and keep the target types such as `Indexer.testIndexerProcessConfig` and
the relevant simulate item types unchanged while making the source side
concrete.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 97d8aea0-7c2c-4df3-a2f3-4bf8dd598396

📥 Commits

Reviewing files that changed from the base of the PR and between 974005e and b1b10d8.

📒 Files selected for processing (2)
  • scenarios/test_codegen/test/SimulateDynamicAddress_test.res
  • scenarios/test_codegen/test/WildcardSimulate_test.res
🚧 Files skipped from review as they are similar to previous changes (1)
  • scenarios/test_codegen/test/WildcardSimulate_test.res

…n tests

Address review feedback on the simulate srcAddress validation:
- WildcardSimulate: assert the full returned change instead of just
  changes.length.
- SimulateDynamicAddress: a srcAddress registered by an earlier process()
  call is accepted via the live indexing-address state; same-batch
  register-then-use stays rejected, documenting that the pre-flight check
  can't see registrations produced during the same run.

Build the simulate items with the typed Indexer.makeSimulateItem helper so
the tests need no Utils.magic on the input side.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175WwoErWZNsba95RnrxRyb
@DZakh
DZakh force-pushed the claude/hopeful-cannon-0243na branch from b1b10d8 to 9593d60 Compare June 29, 2026 13:47
onError now logs the failure once via ErrorHandling.log (preserving the
contextual msg and chain-scoped child logger) and rejects the run wrapped
in Main.FatalError. Callers recognise the wrapper as already-logged:
Bin.res exits without re-logging (no double log), the test worker unwraps
and re-throws the inner error to the parent so test assertions still see
the real message. Setup throws (e.g. patchConfig validation) arrive raw and
are logged by Bin.res / surfaced by the worker as before.

Also read onErrorReject via Option.getUnsafe — it's filled synchronously by
Promise.make before the indexer can call onError, so the None branch was
unreachable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175WwoErWZNsba95RnrxRyb
@DZakh
DZakh enabled auto-merge (squash) June 29, 2026 14:07
@DZakh
DZakh merged commit 0b5a669 into main Jun 29, 2026
8 checks passed
@DZakh
DZakh deleted the claude/hopeful-cannon-0243na branch June 29, 2026 14:19

This branch was previously deployed

1 inactive deployment
internal — ff92932c Deployed Jun 29, 2026 by DZakh via authorize #1908
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.

2 participants