Skip to content

Run user-facing indexer tests from config + source strings - #1488

Merged
DZakh merged 8 commits into
mainfrom
claude/create-test-indexer-method-9loidn
Jul 27, 2026
Merged

Run user-facing indexer tests from config + source strings#1488
DZakh merged 8 commits into
mainfrom
claude/create-test-indexer-method-9loidn

Conversation

@DZakh

@DZakh DZakh commented Jul 24, 2026

Copy link
Copy Markdown
Member

Summary

InternalTestIndexer.fromUserApi gains a ~test argument: a real user-facing test module, type-checked against the config's generated types and then executed. Combined with ~handlers, a fixture can exercise an indexer end to end from YAML alone — no codegen'd project on disk.

Note: an earlier revision of this PR added a registration scope override (withScope, scopeLock, registrationScopeOverride) to envio. All of it was reverted. Priming the parsed config is sufficient, so the test machinery needs no production changes. Reviewers reading an older description can ignore it.

How it works

  1. Parse — the addon turns the YAML into a config JSON plus the TypeScript declarations a real project gets from envio codegen.
  2. Type-check — the real TS compiler checks the handlers and test sources against those declarations, so they are bound to this fixture's chains, contracts, events and entities.
  3. PrimeConfig.prime puts the parsed config where Config.load() finds it, which is what lets the real createTestIndexer from envio run with no project on disk.
  4. Evaluate — both sources are written to modules under test/.tmp and imported. Handlers register through the ordinary global registry.
  5. Collect — the imports happen inside a describe callback, which vitest awaits while building the suite tree, so the test module's it() calls become real tests with proper names, code frames and diffs. Nothing needs to be awaited by the caller, so fixtures stay ordinary _test.res files.

Setup failures (type errors, a duplicate fixture in one file) are reported as a named test rather than a collection crash, and a sentinel test asserts the imports actually ran — if vitest ever stops awaiting suite callbacks the suite would otherwise shrink silently.

Production change

One, independent of the test machinery: registering a handler for an event that no configured chain defines now throws at the registration call site instead of silently never running. Applies to onEvent, onInstruction and contractRegister.

Event "Nonexistent" is not configured on contract "ERC20", so its handler would never run.
Add it to your config, or remove the registration. Configured events on "ERC20": Approval, Transfer.

This is breaking for a project with a dead handler — one registered for a contract absent from every chain, or an event since removed from config — which goes from silently ignored to a startup failure. Worth a changelog note, and it argues for a minor rather than a patch. Happy to split it into its own PR if preferred.

Migrated scenarios

Scenario tests covering test-indexer mechanics (rather than a specific project's handlers) moved to fixtures. They no longer need a codegen'd project, and their handler and test source are now type-checked:

Moved Covers
SimulateDynamicAddress (7) contractRegister, dynamic addresses, repeated process(), address normalization, dead simulate inputs
OptionalBlockParams (9) block-range defaults, progress carryover, auto-exit, invalid ranges
WildcardSimulate (2) wildcard routing regardless of srcAddress
SlotResume (1, svm) onSlot-only indexer progressing after a resume

Left in place deliberately: CustomSelection asserts generated ReScript types (which this path doesn't cover), and LoadLinkedEntities / EventHandler.test.ts exercise test_codegen's own handler graph.

Also dropped rpc: blocks from configs whose tests aren't about sources — chains 1 and 137 resolve a HyperSync endpoint automatically. Kept wherever the block is the subject (env interpolation, URL validation, "unavailable via RPC" field selections, and chain 1337, which has no automatic endpoint).

Known sharp edge

handlers and test are ReScript template strings, so a literal ${ in the TS source must be escaped as \${. Two of the migrated fixtures build entity ids from template literals and do exactly that.

Verification

envio-tests 363 · test_codegen 606 · svm_test 3 — all passing, plus green CI.

Summary by CodeRabbit

  • New Features

    • Enhanced the scenario test harness to support running full indexer suites with optional user test modules and assertions.
    • Added new end-to-end test scenarios (including token transfer/account balance indexing).
  • Bug Fixes

    • Improved event/contract registration errors when no configured match exists across all configured chains.
  • Tests

    • Added regression coverage for optional block parameters, dynamic-address routing, wildcard simulation, and slot resume.
    • Improved test setup/cleanup for generated temp files; simplified test configs by removing explicit RPC sync blocks.
    • Removed outdated generated scenario test files.

https://claude.ai/code/session_014pvXzzaAX22qYRmGCKbbL6

Run handler source in-process against an isolated per-instance registration
so independent test indexers over different configs don't share the global
registry.

- EnvioGlobal: registrationScopeOverride slot resolved ahead of the global
  activeRegistration.
- HandlerRegister: make/finish/withScope build and run a registration record
  detached from the global; isWildcard/getSimulateOnEventRegistrations take it
  explicitly; a scoped registration for an unconfigured event throws.
- SimulateItems.patchConfig threads the active registration through simulate.
- Main: indexer getters resolve the active scope's config when set.
- TestIndexer: extract parameterized make; public createTestIndexer wraps it.
- InternalTestIndexer.createTestIndexer writes the handler source to a unique
  temp file and imports it under the scope, guarding stray pre-registrations.

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

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Internal test indexers now type-check and execute optional user test modules through temporary files and asynchronous Vitest suites. New fixtures cover token balances, simulation behavior, block parameters, wildcard routing, and slot resumption. Handler registration reports missing configured contracts or events.

Changes

Indexer testing and validation

Layer / File(s) Summary
User fixture execution
packages/envio-tests/test/helpers/InternalTestIndexer.res, packages/envio-tests/test/helpers/TypeChecker.ts, packages/envio-tests/test/helpers/globalSetup.ts, packages/envio-tests/vitest.config.ts, packages/envio-tests/.gitignore
User handlers and tests are type-checked, written to temporary modules, imported through asynchronous Vitest suites, and cleaned before test execution.
Unmatched registration errors
packages/envio/src/HandlerRegister.res, packages/envio-tests/test/HandlerRegister_test.res
Registration detects missing contracts or events across chains, throws descriptive errors, and tests configured-name reporting.
Simulation and resume fixtures
packages/envio-tests/test/TokenIndexer_test.res, packages/envio-tests/test/OptionalBlockParams_test.res, packages/envio-tests/test/SimulateDynamicAddress_test.res, packages/envio-tests/test/WildcardSimulate_test.res, packages/envio-tests/test/SlotResume_test.res
Fixtures validate balance accumulation, block-range defaults, dynamic and wildcard event routing, simulated input validation, and resumed slot processing.
Configuration fixture normalization
packages/envio-tests/test/UserApiValidation_test.res, packages/envio-tests/test/lib_tests/ColumnNameFormat_test.res, scenarios/test_codegen/test/lib_tests/ClickHouse_test.res
Embedded test configurations remove explicit per-chain RPC synchronization blocks.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Fixture
  participant InternalTestIndexer
  participant TypeChecker
  participant HandlerRegister
  participant Vitest
  Fixture->>InternalTestIndexer: Provide config, handlers, and test source
  InternalTestIndexer->>TypeChecker: Validate virtual handler and test modules
  InternalTestIndexer->>HandlerRegister: Start registration with parsed config
  InternalTestIndexer->>Vitest: Import temporary modules
  Vitest->>HandlerRegister: Register event handlers
  Vitest->>Vitest: Execute simulation and assertion cases
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the main change: running indexer tests from config and source strings via the test indexer flow.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e885fcf4e1

ℹ️ 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 thread scenarios/test_codegen/test/__mocks__/MockConfig.res Outdated
Comment thread packages/envio/src/HandlerRegister.res Outdated
Replace the scoped-registration approach with `defineIndexerTest`, which
evaluates a real user-facing test module against a config parsed from YAML.

Priming the config (`Config.prime`) is enough to make the public
`createTestIndexer` from "envio" work with no project on disk, so handlers
register through the normal global registry and none of the previous envio
production changes are needed. This reverts the registration scope override,
`withScope`, the indexer getter changes, and the SimulateItems/TestIndexer
threading.

`handlers` and `test` are ordinary user modules, type-checked against the
config's generated types and then executed. The test module's `it()` calls
register into the calling file's suite, so failures get real vitest names,
code frames and diffs; the suite is named for the `defineIndexerTest` call
site, and setup failures surface as a named test rather than a collection
crash. Generated modules persist for the run so code frames resolve, and
`globalSetup` clears them beforehand.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014pvXzzaAX22qYRmGCKbbL6
A registration that matches no configured chain can never be dispatched, so
the handler silently never runs. Report it at the registration call site
instead, naming the configured contracts or events so the typo is obvious.
Applies to onEvent, onInstruction and contractRegister alike.

Also move defineIndexerTest into InternalTestIndexer.res — only the TS
compiler harness needs to be TypeScript. The fixture file stays .test.ts
since collection-time registration needs top-level await.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014pvXzzaAX22qYRmGCKbbL6
One entry point: `fromUserApi(~test)` type-checks and runs a user-facing test
module, instead of a separate `defineIndexerTest`.

Vitest awaits a suite callback while collecting, so registering the imports
inside `describe` keeps `fromUserApi` synchronous — no top-level await, which
means fixtures are ordinary `_test.res` files rather than `.test.ts`.

Type errors and setup failures are reported as a named test rather than a
collection crash, since the parse already succeeded and the config is still
returned. Parse-only callers keep throwing, as their assertions expect.

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

@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 (2)
packages/envio-tests/test/helpers/InternalTestIndexer.res (2)

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

Stale function name in comment.

The comment refers to defineIndexerTest, but the actual public entrypoint in this diff is fromUserApi. This looks like a leftover from an earlier naming iteration and could mislead readers about which call this stack-walk is attributing.

✏️ Suggested fix
-// `file:line` of the `defineIndexerTest` call — the first stack frame outside
+// `file:line` of the `fromUserApi` call — the first stack frame outside
 // this helper — so a failing suite names the fixture that produced it.
🤖 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-tests/test/helpers/InternalTestIndexer.res` around lines 34 -
46, Update the explanatory comment above callSite to refer to the actual public
entrypoint fromUserApi instead of defineIndexerTest, leaving the stack-walking
implementation unchanged.

48-48: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicated tmpDir path computation across ReScript and TS. Both files independently re-derive the same test/.tmp location relative to test/helpers/; they currently agree, but nothing keeps them in sync if the directory name or relative depth changes.

  • packages/envio-tests/test/helpers/InternalTestIndexer.res#L48: consider sourcing the .tmp directory name from a single constant (e.g. exported from TypeChecker.ts or a small shared JSON/config) that both this file and globalSetup.ts import.
  • packages/envio-tests/test/helpers/globalSetup.ts#L10: same — derive from the same shared constant instead of recomputing the relative path independently.
🤖 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-tests/test/helpers/InternalTestIndexer.res` at line 48,
Centralize the temporary directory path used by InternalTestIndexer and
globalSetup instead of computing it independently. Update
packages/envio-tests/test/helpers/InternalTestIndexer.res:48 and
packages/envio-tests/test/helpers/globalSetup.ts:10 to consume one shared
exported constant or configuration value for the test/.tmp location, preserving
the existing resolved directory.
🤖 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 `@packages/envio-tests/test/helpers/InternalTestIndexer.res`:
- Around line 34-46: Update the explanatory comment above callSite to refer to
the actual public entrypoint fromUserApi instead of defineIndexerTest, leaving
the stack-walking implementation unchanged.
- Line 48: Centralize the temporary directory path used by InternalTestIndexer
and globalSetup instead of computing it independently. Update
packages/envio-tests/test/helpers/InternalTestIndexer.res:48 and
packages/envio-tests/test/helpers/globalSetup.ts:10 to consume one shared
exported constant or configuration value for the test/.tmp location, preserving
the existing resolved directory.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3e2711c0-8817-4e1a-be1d-3c136739738d

📥 Commits

Reviewing files that changed from the base of the PR and between dc505fd and 37727bd.

📒 Files selected for processing (8)
  • packages/envio-tests/.gitignore
  • packages/envio-tests/test/HandlerRegister_test.res
  • packages/envio-tests/test/TokenIndexer_test.res
  • packages/envio-tests/test/helpers/InternalTestIndexer.res
  • packages/envio-tests/test/helpers/TypeChecker.ts
  • packages/envio-tests/test/helpers/globalSetup.ts
  • packages/envio-tests/vitest.config.ts
  • packages/envio/src/HandlerRegister.res

Chain 1 and 137 resolve a HyperSync endpoint automatically, so the rpc
block was noise in configs whose tests are about handlers, entities,
addresses or event signatures.

Kept where the block is the subject: env-var interpolation, RPC URL and
WebSocket validation, dual-source rejection, "unavailable via RPC" field
selections, and chain 1337, which has no automatic endpoint.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014pvXzzaAX22qYRmGCKbbL6
The imported test module only registers if vitest awaits the suite callback
during collection. If that ever stops holding the suite shrinks silently,
which no reporter flags, so register a sentinel test outside the callback
that asserts the import ran.

Move the scenario tests that cover test-indexer mechanics rather than a
specific project's handlers, which no longer need a codegen'd project and
now get their handler and test source type-checked:

- SimulateDynamicAddress (7) — contractRegister, dynamic addresses,
  repeated process(), address normalization, dead simulate inputs
- OptionalBlockParams (9) — block-range defaults, progress carryover,
  auto-exit, invalid ranges
- WildcardSimulate (2) — wildcard routing regardless of srcAddress
- SlotResume (1, svm) — onSlot-only indexer progressing after resume

Left behind: CustomSelection asserts generated ReScript types, and
LoadLinkedEntities and EventHandler.test.ts exercise test_codegen's own
handlers — none are test-indexer mechanics.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014pvXzzaAX22qYRmGCKbbL6
checkHandlerTypes lost its callers when checkSources landed, the
publicConfigJson field on parsed was never read outside the helper, and the
name override was never passed. Trim the comments that outlived the code
they described.

Unifying the two type-check paths means the thrown prefix is now "Type
errors:" for handlers as well, and the throw assertions move to main's
stricter toThrowErrorEqual.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014pvXzzaAX22qYRmGCKbbL6
@DZakh DZakh changed the title Support isolated handler registration for internal test indexer Run user-facing indexer tests from config + source strings Jul 27, 2026
@DZakh
DZakh merged commit d20dfd8 into main Jul 27, 2026
8 checks passed
@DZakh
DZakh deleted the claude/create-test-indexer-method-9loidn branch July 27, 2026 14:52

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c1a0254364

ℹ️ 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".

// Nothing matched on any chain, so the callback could never be dispatched.
// Reported at the registration call site, where the stack still points at the
// offending `onEvent`/`contractRegister`.
if !matched.contents {

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 Allow handlers that belong to skipped chains

When a contract or event exists only on a chain marked skip: true while another chain remains active, to_public_config_json removes that chain from the runtime chainMap but retains the global contract definitions and their handler files. The handler is therefore still loaded, yet this check sees no active-chain match and aborts indexer startup. This breaks the supported workflow of temporarily disabling a chain without deleting its handlers; no-match registrations should remain lenient for the global loader or account for skipped-chain definitions.

Useful? React with 👍 / 👎.

// sibling worker's files mid-run.
export default function setup(): void {
const tmpDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", ".tmp");
fs.rmSync(tmpDir, { recursive: true, force: true });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Isolate temporary modules per Vitest run

When two Vitest commands run concurrently in the same checkout, both use this shared test/.tmp directory and each global setup recursively deletes it. One run can therefore remove handler or test modules after the other run writes them but before its deferred describe callback imports them, producing intermittent module-not-found collection failures; use a per-run directory or coordinate cleanup instead of deleting the shared directory.

Useful? React with 👍 / 👎.

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