Skip to content

Add simulate API to test indexer for processing mock events#1040

Merged
DZakh merged 60 commits into
mainfrom
claude/add-indexer-test-api-XcpxD
Mar 31, 2026
Merged

Add simulate API to test indexer for processing mock events#1040
DZakh merged 60 commits into
mainfrom
claude/add-indexer-test-api-XcpxD

Conversation

@DZakh

@DZakh DZakh commented Mar 16, 2026

Copy link
Copy Markdown
Member

Summary

This PR adds a simulate option to the test indexer's process() API, allowing users to specify mock events and block handlers to process without fetching from real data sources. This enables deterministic testing of event handlers with predefined data.

Key Changes

  • New SimulateItems.res module: Parses user-provided simulate items (events and block handlers) into internal Internal.item format. Handles:

    • Event items: validates contract/event names against config, parses params using event schema, applies defaults for block number, log index, source address, and transaction/block data
    • Block items: validates handler names exist in registrations, applies defaults for block number
    • Comprehensive error messages for invalid configurations
  • New SimulateSource.res module: Implements the Source.t interface to provide pre-built items without network calls. Filters items by block range and returns them as a response.

  • Updated Main.res: Integrates simulate support into the indexer startup flow. When processConfig contains simulate items for a chain, creates a SimulateSource and overrides the chain's source configuration to use it instead of fetching from real sources.

  • Updated TestIndexer.res: Passes processConfig (containing simulate items) through worker data to the indexer process. Adds BigInt-safe JSON serialization to handle BigInt values in simulate params.

  • TypeScript definitions (index.d.ts): Added SimulateEventItem, SimulateBlockItem, and SimulateItem types, plus optional simulate field to TestIndexerChainConfig.

  • Test coverage: Added two test cases demonstrating:

    • Processing a single simulated event and verifying entity changes
    • Processing multiple simulated events in sequence

Implementation Details

  • Auto-incrementing log indices: Log indices auto-increment per block when not explicitly provided, creating realistic event ordering
  • Default values: Block numbers default to startBlock, source addresses default to the first contract address from config, timestamps default to 0
  • Error handling: Runtime validation ensures contract/event names and block handler names exist in the configuration, with helpful error messages listing available options
  • No network calls: Simulate sources bypass all network I/O, making tests fast and deterministic

https://claude.ai/code/session_012sjgbBChpXi5yVFE8byZN4

Summary by CodeRabbit

  • New Features

    • Simulate-driven test runner: feed event/block simulations per-chain and apply a per-run config patch hook.
    • Generated contract typings now expose per-contract event names for richer type-aware tooling.
    • Added a simulate-backed source to inject parsed simulate items into the runtime.
  • Tests

    • Updated tests and templates to use a test-indexer harness with simulate lists (replacing prior mock-db flows).
    • Added tests validating simulate processing and end-to-end entity updates.
  • Chores

    • BigInt-safe JSON handling for test worker data.

claude added 2 commits March 5, 2026 20:50
Allows test indexer to process user-specified events without fetching from
real sources. When `simulate` is provided in a chain's process config,
a SimulateSource replaces real chain sources and returns only the specified items.

- SimulateSource: Source.t implementation that returns pre-built items
- SimulateItems: Parses simulate config into Internal.item using event schemas
- Main.start: Accepts optional processConfig to override chain sources
- TestIndexer: Passes processConfig to worker with BigInt-safe serialization
- TS types: SimulateItem, SimulateEventItem, SimulateBlockItem in index.d.ts

https://claude.ai/code/session_012sjgbBChpXi5yVFE8byZN4
@coderabbitai

coderabbitai Bot commented Mar 16, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a test simulate pipeline: parse simulate items from processConfig into internal items, attach per-chain SimulateSource instances via a patchConfig hook passed to Main.start, and route workers to consume those simulated events/blocks for test indexer runs.

Changes

Cohort / File(s) Summary
TypeDefs / Codegen
packages/envio/index.d.ts, packages/cli/src/hbs_templating/codegen_templates.rs, packages/cli/templates/dynamic/codegen/index.d.ts.hbs
Typed evm.contracts/fuel.contracts now include an events union; codegen emits per-contract events lists; TestHelpers export shape narrowed.
Simulate Parsing & Patch
packages/envio/src/SimulateItems.res
New module: validates raw simulate JSON, resolves event schemas, parses params, infers block/log indices and srcAddress, exposes parse, findEventConfig, and patchConfig.
Simulate Source
packages/envio/src/sources/SimulateSource.res
New SimulateSource.make(~items, ~endBlock, ~chain) returning a Source.t that provides height and filtered items for requested ranges.
Startup / Patch Hook
packages/envio/src/Main.res, packages/envio/src/TestIndexer.res
Main.start gains optional ~patchConfig parameter; TestIndexer threads serialize processConfig (BigInt-safe), forward it to workers, and supply a patchConfig to apply simulate sources at startup.
Worker / Dataflow
packages/envio/src/TestIndexer.res, packages/envio/src/Main.res
Worker initialization and workerData updated to carry processConfig; Main/worker flows altered to apply patching so test runs use SimulateSource-based chain sources.
Templates / Tests
packages/cli/templates/**/*, scenarios/test_codegen/**/*, scenarios/**/test/*, packages/cli/templates/static/*
Removed generated MockDb/TestHelpers modules and templates; test templates rewritten to use createTestIndexer() + indexer.process({ chains: { ... simulate: [...] } }) with updated assertions; many test files updated accordingly.
Plan / Docs
plan.md
Documents simulate API, codegen changes, and migration plan for moving simulate setup into TestIndexer flow.

Sequence Diagram(s)

sequenceDiagram
    rect rgba(220,240,255,0.5)
    participant TI as TestIndexer
    participant Main as Main.start
    participant Parser as SimulateItems.parse
    participant Source as SimulateSource.make
    participant Worker as Indexer Worker
    participant Handler as Event Handler
    end

    TI->>Main: start(processConfig)
    Main->>Parser: parse(simulateItems, config, chainConfig, registrations)
    Parser-->>Main: Internal.item[] (validated)
    Main->>Source: make(items, endBlock, chain)
    Source-->>Main: Source.t (simulate)
    Main->>Worker: start with patched chainMap (CustomSources)
    Worker->>Source: getItemsOrThrow(fromBlock, endBlock)
    Source-->>Worker: filtered parsed items
    Worker->>Handler: process items
    Handler-->>Worker: entity changes applied
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • JonoPrest
  • moose-code

Poem

🐇
I hopped through events and blocks with care,
Stashed them in memory for tests to share,
No network chase — just simulated air,
Handlers munched logs with a joyful stare. 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and accurately describes the main change: adding a simulate API to the test indexer for processing mock events.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/add-indexer-test-api-XcpxD

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 and usage tips.

@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: 5

🧹 Nitpick comments (2)
scenarios/test_codegen/test/EventHandler.test.ts (1)

958-1054: Add regression cases for the new defaulting rules.

These tests only exercise startBlock: 1 on a single block. Please add one case where process().chains[*].startBlock is higher than the chain's config start block and another where events span two blocks without explicit logIndex, otherwise the new default block-number/log-index behavior can regress unnoticed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scenarios/test_codegen/test/EventHandler.test.ts` around lines 958 - 1054,
Add two regression tests to EventHandler.test.ts using createTestIndexer and
indexer.process: (1) a case where process({chains: {X: {startBlock: N}}}) uses a
startBlock N greater than the chain's configured default start block to verify
defaulting/skip behavior; and (2) a case where simulate supplies events across
two consecutive blocks (e.g., block 1 and block 2) without explicit logIndex
values to ensure block-number/log-index defaulting works when events span
multiple blocks. For both tests assert result.changes length, eventsProcessed
count, and entity sets (using indexer.<Entity>.get) to validate persisted state
and prevent regressions in the new defaulting rules.
packages/envio/src/TestIndexer.res (1)

563-575: Annotate or remove this Utils.magic cast.

Line 572 is now part of the worker serialization boundary, and it is the only new Utils.magic here without an explicit source/target type. Please make that cast explicit, or drop it if it is unnecessary, so this path stays auditable. As per coding guidelines, "When using Utils.magic for type casting in ReScript, always add explicit type annotations: value->(Utils.magic: inputType => outputType)".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/envio/src/TestIndexer.res` around lines 563 - 575, The cast on
initialState using Utils.magic inside workerDataObj is unannotated and must be
made explicit (or removed if unnecessary); update the expression
"initialState->Utils.magic" to a typed cast like initialState->(Utils.magic:
sourceType => targetType) with the correct sourceType/targetType that matches
the worker serialization boundary (or delete the Utils.magic if initialState
already has the proper type), ensuring this change is made next to workerDataObj
so the serialization via jsonStringifyBigIntSafe and subsequent Js.Json.parseExn
remains type-safe and auditable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/envio/src/SimulateItems.res`:
- Around line 109-120: The code currently replaces the entire block/transaction
when item.block or item.transaction is Some(...), causing partial overrides
(e.g. {timestamp:123}) to drop defaults like blockNumber; change the logic in
the block and transaction builders to shallow-merge the provided override onto
the default object instead of replacing it: create the default block ({"number":
blockNumber, "timestamp": 0}) and default transaction (Js.Dict.empty()) and then
merge fields from the override Js.Json/Dict into those defaults before casting
with Utils.magic to Internal.eventBlock / Internal.eventTransaction, ensuring
defaults like blockNumber are preserved when only partial overrides are
supplied.
- Around line 54-55: currentLogIndex is a single global ref so logIndex does not
reset per block; change it to a per-block counter (e.g., a Map/Js.Dict keyed by
the block identifier) and replace usages that read/increment currentLogIndex to
instead read the counter for the current block (default 0) and store back the
incremented value for that block; update the declaration (currentLogIndex) and
all sites that use it (including the other occurrence around lines 81-91) so
omitted logIndex values are auto-assigned starting at 0 for each block
independently.
- Around line 26-42: The current findEventConfig walks every chain in
config.chainMap and uses a ref to capture success; change it to resolve only
within the provided chainConfig and remove the ref by using a search expression
(try/with or Belt.Array.getBy) that returns an option. Concretely, update
findEventConfig (or its signature) to accept/operate on the specific chainConfig
(use chainConfig.contracts->Belt.Array.getBy(c => c.name === contractName) to
find the contract, then contract.events->Belt.Array.getBy(e => e.name ===
eventName) to find the event) and return Some(eventConfig) or None; replace the
ref-based mutation with a try/with or direct option-returning expressions so no
ref is needed.
- Around line 44-53: The parse function in SimulateItems.res currently defaults
missing item.block numbers to chainConfig.startBlock (chainConfig.startBlock)
which is the static chain config; change parse(~simulateItems, ~config,
~chainConfig, ~registrations) to accept an additional per-process startBlock
parameter (e.g., ~processStartBlock) and use that value instead of
chainConfig.startBlock when an individual simulate item omits number so items
are created at the correct window; update the caller in Main.res to pass the
per-process startBlock through to SimulateItems.parse and ensure
SimulateSource.res filtering behavior continues to work with the new default
semantics.

In `@packages/envio/src/sources/SimulateSource.res`:
- Around line 15-52: The getItemsOrThrow implementation currently filters items
and sets latestFetchedBlockNumber using endBlock, ignoring the requested toBlock
parameter; update the filtering and checkpoint logic to respect toBlock by
replacing the use of endBlock with the toBlock parameter in the range check (use
Internal.getItemBlockNumber <= toBlock) and ensure latestFetchedBlockNumber is
set no higher than toBlock (e.g., compute the max blockNumber of filteredItems
but clamp it to toBlock, or use toBlock if no items were returned); update
references in getItemsOrThrow (filteredItems, latestFetchedBlockNumber,
Internal.getItemBlockNumber) accordingly so the source does not leak later
simulated items or advance checkpoints past toBlock.

---

Nitpick comments:
In `@packages/envio/src/TestIndexer.res`:
- Around line 563-575: The cast on initialState using Utils.magic inside
workerDataObj is unannotated and must be made explicit (or removed if
unnecessary); update the expression "initialState->Utils.magic" to a typed cast
like initialState->(Utils.magic: sourceType => targetType) with the correct
sourceType/targetType that matches the worker serialization boundary (or delete
the Utils.magic if initialState already has the proper type), ensuring this
change is made next to workerDataObj so the serialization via
jsonStringifyBigIntSafe and subsequent Js.Json.parseExn remains type-safe and
auditable.

In `@scenarios/test_codegen/test/EventHandler.test.ts`:
- Around line 958-1054: Add two regression tests to EventHandler.test.ts using
createTestIndexer and indexer.process: (1) a case where process({chains: {X:
{startBlock: N}}}) uses a startBlock N greater than the chain's configured
default start block to verify defaulting/skip behavior; and (2) a case where
simulate supplies events across two consecutive blocks (e.g., block 1 and block
2) without explicit logIndex values to ensure block-number/log-index defaulting
works when events span multiple blocks. For both tests assert result.changes
length, eventsProcessed count, and entity sets (using indexer.<Entity>.get) to
validate persisted state and prevent regressions in the new defaulting rules.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bfb62a1f-6dff-42fb-ab8e-e1274a2223a3

📥 Commits

Reviewing files that changed from the base of the PR and between e52550a and 942da15.

⛔ Files ignored due to path filters (2)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • scenarios/test_codegen/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (8)
  • packages/envio/bin.mjs
  • packages/envio/index.d.ts
  • packages/envio/src/Main.res
  • packages/envio/src/SimulateItems.res
  • packages/envio/src/TestIndexer.res
  • packages/envio/src/sources/SimulateSource.res
  • plan.md
  • scenarios/test_codegen/test/EventHandler.test.ts

Comment thread packages/envio/src/SimulateItems.res
Comment thread packages/envio/src/SimulateItems.res
Comment thread packages/envio/src/SimulateItems.res
Comment thread packages/envio/src/SimulateItems.res Outdated
Comment on lines +15 to +52
getItemsOrThrow: (
~fromBlock,
~toBlock as _,
~addressesByContractName as _,
~indexingContracts as _,
~knownHeight as _,
~partitionId as _,
~selection as _,
~retry as _,
~logger as _,
) => {
let filteredItems = []
for i in 0 to items->Array.length - 1 {
let item = items->Js.Array2.unsafe_get(i)
let blockNumber = item->Internal.getItemBlockNumber
if blockNumber >= fromBlock && blockNumber <= endBlock {
filteredItems->Array.push(item)->ignore
}
}

let latestFetchedBlockNumber = endBlock
Promise.resolve({
Source.knownHeight: endBlock,
reorgGuard: {
rangeLastBlock: {
blockHash: "0x0000000000000000000000000000000000000000000000000000000000000000",
blockNumber: latestFetchedBlockNumber,
},
prevRangeLastBlock: None,
},
parsedQueueItems: filteredItems,
fromBlockQueried: fromBlock,
latestFetchedBlockNumber,
latestFetchedBlockTimestamp: 0,
stats: {
totalTimeElapsed: 0,
},
})

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.

⚠️ Potential issue | 🔴 Critical

Respect toBlock in getItemsOrThrow.

Lines 27-47 filter against endBlock only and then report latestFetchedBlockNumber = endBlock. If the fetcher asks this source for a paged subrange, this implementation will leak later simulated items into the current batch and advance checkpoints too far.

Suggested fix
   getItemsOrThrow: (
     ~fromBlock,
-    ~toBlock as _,
+    ~toBlock,
     ~addressesByContractName as _,
     ~indexingContracts as _,
     ~knownHeight as _,
     ~partitionId as _,
     ~selection as _,
     ~retry as _,
     ~logger as _,
   ) => {
+    let upperBound = toBlock->Option.getWithDefault(endBlock)
     let filteredItems = []
     for i in 0 to items->Array.length - 1 {
       let item = items->Js.Array2.unsafe_get(i)
       let blockNumber = item->Internal.getItemBlockNumber
-      if blockNumber >= fromBlock && blockNumber <= endBlock {
+      if blockNumber >= fromBlock && blockNumber <= upperBound {
         filteredItems->Array.push(item)->ignore
       }
     }
 
-    let latestFetchedBlockNumber = endBlock
+    let latestFetchedBlockNumber = upperBound
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/envio/src/sources/SimulateSource.res` around lines 15 - 52, The
getItemsOrThrow implementation currently filters items and sets
latestFetchedBlockNumber using endBlock, ignoring the requested toBlock
parameter; update the filtering and checkpoint logic to respect toBlock by
replacing the use of endBlock with the toBlock parameter in the range check (use
Internal.getItemBlockNumber <= toBlock) and ensure latestFetchedBlockNumber is
set no higher than toBlock (e.g., compute the max blockNumber of filteredItems
but clamp it to toBlock, or use toBlock if no items were returned); update
references in getItemsOrThrow (filteredItems, latestFetchedBlockNumber,
Internal.getItemBlockNumber) accordingly so the source does not leak later
simulated items or advance checkpoints past toBlock.

claude added 2 commits March 16, 2026 10:11
Codegen now emits contract event names into EvmContracts/FuelContracts types
(e.g. `"Gravatar": { events: "NewGravatar" | "UpdatedGravatar" }`), enabling
TypeScript to validate simulate items' contract+event pairs at compile time.

https://claude.ai/code/session_012sjgbBChpXi5yVFE8byZN4

@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: 1

🧹 Nitpick comments (2)
packages/cli/src/hbs_templating/codegen_templates.rs (2)

1920-1925: Handle empty events case to prevent invalid TypeScript syntax.

If a contract has no events, event_names.join(" | ") produces an empty string, generating invalid TypeScript: { events: };. Consider using never as a fallback:

Proposed fix
                         let event_names: Vec<String> = contract
                             .events
                             .iter()
                             .map(|event| format!("\"{}\"", event.name))
                             .collect();
-                        format!("  \"{}\": {{ events: {} }};", name, event_names.join(" | "))
+                        let events_union = if event_names.is_empty() {
+                            "never".to_string()
+                        } else {
+                            event_names.join(" | ")
+                        };
+                        format!("  \"{}\": {{ events: {} }};", name, events_union)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/hbs_templating/codegen_templates.rs` around lines 1920 -
1925, The generated TypeScript can be invalid when a contract has no events
because event_names.join(" | ") becomes empty; update the code that builds
event_names and the format! call (the block creating let event_names:
Vec<String> and the subsequent format!("  \"{}\": {{ events: {} }};", name,
...)) to fall back to "never" when event_names is empty (e.g., compute a
variable events_type = if event_names.is_empty() { "never".to_string() } else {
event_names.join(" | ") } and use events_type in the format! string) so the
emitted snippet becomes "{ events: never }" instead of "{ events: }".

1970-1975: Same empty events edge case applies here.

Apply the same defensive handling as suggested for EvmContracts above.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/hbs_templating/codegen_templates.rs` around lines 1970 -
1975, The current template always builds an events union from contract.events
which yields an invalid/empty union when there are no events; mirror the
defensive fix used for EvmContracts by checking contract.events.is_empty(): if
empty, set the events type to "never" (or another sentinel used in EvmContracts)
instead of building event_names, otherwise build event_names as now (map/format
and join with " | "); then use that events_type in the format!("  \"{}\": {{
events: {} }};", name, events_type) expression. Ensure you reference and replace
the existing event_names creation and the final format call.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/envio/index.d.ts`:
- Around line 572-585: The SimulateItemForConfig conditional currently returns
never when Config["evm"]?.contracts or Config["fuel"]?.contracts is undefined,
blocking SimulateBlockItem; update SimulateItemForConfig so that in the HasEvm
branch you return (EvmSimulateEventItem<Config["evm"]["contracts"]> |
SimulateBlockItem) when Config["evm"]["contracts"] extends Record<...> OR when
Config["evm"]["contracts"] is undefined (i.e., allow SimulateBlockItem for
contract-less configs), and do the analogous change in the HasFuel branch to
return (FuelSimulateEventItem<Config["fuel"]["contracts"]> | SimulateBlockItem)
when contracts exist or undefined, ensuring the conditional checks around
Config["evm"]["contracts"] and Config["fuel"]["contracts"] permit the block-only
case rather than yielding never.

---

Nitpick comments:
In `@packages/cli/src/hbs_templating/codegen_templates.rs`:
- Around line 1920-1925: The generated TypeScript can be invalid when a contract
has no events because event_names.join(" | ") becomes empty; update the code
that builds event_names and the format! call (the block creating let
event_names: Vec<String> and the subsequent format!("  \"{}\": {{ events: {}
}};", name, ...)) to fall back to "never" when event_names is empty (e.g.,
compute a variable events_type = if event_names.is_empty() { "never".to_string()
} else { event_names.join(" | ") } and use events_type in the format! string) so
the emitted snippet becomes "{ events: never }" instead of "{ events: }".
- Around line 1970-1975: The current template always builds an events union from
contract.events which yields an invalid/empty union when there are no events;
mirror the defensive fix used for EvmContracts by checking
contract.events.is_empty(): if empty, set the events type to "never" (or another
sentinel used in EvmContracts) instead of building event_names, otherwise build
event_names as now (map/format and join with " | "); then use that events_type
in the format!("  \"{}\": {{ events: {} }};", name, events_type) expression.
Ensure you reference and replace the existing event_names creation and the final
format call.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 31c4eb0c-9969-4d1f-bf9f-8b0d7d9e9d52

📥 Commits

Reviewing files that changed from the base of the PR and between cf7e4f7 and 4a10aa4.

⛔ Files ignored due to path filters (2)
  • packages/cli/src/hbs_templating/snapshots/envio__hbs_templating__codegen_templates__test__envio_dts_code_generated_for_evm.snap is excluded by !**/*.snap
  • packages/cli/src/hbs_templating/snapshots/envio__hbs_templating__codegen_templates__test__envio_dts_code_generated_for_fuel.snap is excluded by !**/*.snap
📒 Files selected for processing (3)
  • packages/cli/src/hbs_templating/codegen_templates.rs
  • packages/envio/index.d.ts
  • plan.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • plan.md

Comment thread packages/envio/index.d.ts Outdated
- Extract simulate config patching logic into SimulateItems.patchConfig
- Main.start takes optional ~patchConfig callback instead of ~processConfig
- TestIndexer constructs patchConfig closure using SimulateItems.patchConfig
- Split SimulateItemForConfig into per-ecosystem types (EvmSimulateItem,
  FuelSimulateItem, SvmSimulateItem) with ecosystem-specific chain configs

https://claude.ai/code/session_012sjgbBChpXi5yVFE8byZN4

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

♻️ Duplicate comments (3)
packages/envio/src/SimulateItems.res (3)

26-42: ⚠️ Potential issue | 🟠 Major

findEventConfig still searches all chains instead of the target chain.

The function iterates over config.chainMap->ChainMap.values, meaning a simulate item targeting chain A can be accepted if the contract/event exists only on chain B. This bypasses per-chain validation.

Additionally, the ref-based pattern can be replaced with a functional approach.

Proposed refactor to search only the target chain and eliminate ref

Consider passing chainConfig as a parameter and searching only its contracts:

-let findEventConfig = (~config: Config.t, ~contractName: string, ~eventName: string) => {
-  let found = ref(None)
-  config.chainMap
-  ->ChainMap.values
-  ->Array.forEach(chainConfig => {
-    chainConfig.contracts->Array.forEach(contract => {
-      if contract.name === contractName {
-        contract.events->Array.forEach(eventConfig => {
-          if eventConfig.name === eventName {
-            found := Some(eventConfig)
-          }
-        })
-      }
-    })
-  })
-  found.contents
-}
+let findEventConfig = (~chainConfig: Config.chain, ~contractName: string, ~eventName: string) => {
+  chainConfig.contracts
+  ->Array.getBy(contract => contract.name === contractName)
+  ->Option.flatMap(contract =>
+    contract.events->Array.getBy(eventConfig => eventConfig.name === eventName)
+  )
+}

Then update the call site at line 64 to pass ~chainConfig instead of ~config.

Based on learnings: "Use try/catch as expressions instead of refs for tracking success/failure in ReScript"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/envio/src/SimulateItems.res` around lines 26 - 42, findEventConfig
currently scans config.chainMap->ChainMap.values (all chains) and uses a ref;
change it to accept ~chainConfig: ChainConfig.t and search only
chainConfig.contracts for the matching contract and event, returning an option
directly (no refs). Replace the nested ref logic with a functional search (e.g.,
Array.find/keepMap or chained Array.find on contracts then events) inside
findEventConfig and update its call sites that pass ~config to pass ~chainConfig
instead (the call that currently invokes findEventConfig with ~config should
pass ~chainConfig). Ensure the function signature and callers are updated and
remove the ref and imperative assignment in favor of returning Some(eventConfig)
or None.

44-55: ⚠️ Potential issue | 🟠 Major

Two unresolved issues: startBlock source and per-block logIndex counter.

  1. Line 52: startBlock = chainConfig.startBlock uses the static config value. When indexer.process({ chains: { ... startBlock: N, simulate: [...] }}) runs a later window, items omitting number will be created at the wrong block.

  2. Lines 54-55: currentLogIndex is a single global ref. Items {number: 1} followed by {number: 2} get logIndex 0, 1 instead of resetting to 0 for block 2.

Proposed fix
 let parse = (
   ~simulateItems: array<Js.Json.t>,
   ~config: Config.t,
   ~chainConfig: Config.chain,
+  ~processStartBlock: int,
   ~registrations: HandlerRegister.registrations,
 ): array<Internal.item> => {
   let chain = ChainMap.Chain.makeUnsafe(~chainId=chainConfig.id)
   let chainId = chainConfig.id
-  let startBlock = chainConfig.startBlock
-  let currentBlock = ref(startBlock)
-  let currentLogIndex = ref(0)
+  let startBlock = processStartBlock
+  let currentBlock = ref(startBlock)
+  let logIndexByBlock: Js.Dict.t<int> = Js.Dict.empty()

Then in the logIndex assignment (around lines 85-91):

       let logIndex = switch item.logIndex {
       | Some(li) => li
       | None =>
-        let li = currentLogIndex.contents
-        currentLogIndex := li + 1
+        let blockKey = blockNumber->Int.toString
+        let li = logIndexByBlock->Js.Dict.get(blockKey)->Option.getWithDefault(0)
+        logIndexByBlock->Js.Dict.set(blockKey, li + 1)
         li
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/envio/src/SimulateItems.res` around lines 44 - 55, The parse
function uses chainConfig.startBlock and a single global currentLogIndex,
causing items without a number to get the wrong base block and logIndex not
resetting per block; change initialization to derive the starting block from
simulateItems (find the first item with a `number` and use that as startBlock,
falling back to chainConfig.startBlock) and replace the single global
currentLogIndex with per-block logic: keep currentBlock as a ref and before
assigning a logIndex check the item's block number (or the currentBlock if item
has none) and if it differs from currentBlock reset the log index to 0 and
update currentBlock (use these symbols: parse, chainConfig, simulateItems,
currentBlock, currentLogIndex) so each block's logIndex restarts at 0 and items
without `number` use the intended start block.

109-121: ⚠️ Potential issue | 🟠 Major

Partial block/transaction overrides drop required defaults.

When item.block is Some({timestamp: 123}), the entire default is replaced, losing blockNumber. This causes Internal.Event.blockNumber (line 129) to diverge from event.block.number.

Proposed fix to merge overrides onto defaults
       // Build block and transaction as empty objects with optional overrides
+      let defaultBlock = {"number": blockNumber, "timestamp": 0}
       let block = switch item.block {
-      | Some(b) => b->(Utils.magic: Js.Json.t => Internal.eventBlock)
-      | None =>
-        {"number": blockNumber, "timestamp": 0}->(
-          Utils.magic: {"number": int, "timestamp": int} => Internal.eventBlock
-        )
+      | Some(b) =>
+        // Merge user override onto defaults
+        Js.Obj.assign(
+          defaultBlock->Obj.magic,
+          b->Obj.magic
+        )->(Utils.magic: {..} => Internal.eventBlock)
+      | None => defaultBlock->(Utils.magic: {"number": int, "timestamp": int} => Internal.eventBlock)
       }

+      let defaultTransaction = Js.Dict.empty()
       let transaction = switch item.transaction {
-      | Some(t) => t->(Utils.magic: Js.Json.t => Internal.eventTransaction)
-      | None => Js.Dict.empty()->(Utils.magic: dict<unit> => Internal.eventTransaction)
+      | Some(t) =>
+        Js.Obj.assign(
+          defaultTransaction->Obj.magic,
+          t->Obj.magic
+        )->(Utils.magic: {..} => Internal.eventTransaction)
+      | None => defaultTransaction->(Utils.magic: dict<unit> => Internal.eventTransaction)
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/envio/src/SimulateItems.res` around lines 109 - 121, The issue is
that when building `block` and `transaction` you currently replace the entire
default with `item.block`/`item.transaction`, dropping required defaults like
`blockNumber`; instead, merge the provided override onto the default object so
defaults remain. Concretely, for `block` build a default object (e.g.,
{"number": blockNumber, "timestamp": 0}) and shallow-merge properties from
`item.block` on top of it before casting to `Internal.eventBlock`; do the same
for `transaction` by merging `item.transaction` onto the empty/default
transaction object prior to casting to `Internal.eventTransaction`. Ensure
merging preserves unspecified default fields like `number`.
🧹 Nitpick comments (2)
packages/envio/src/SimulateItems.res (1)

93-107: Consider eliminating ref for srcAddress lookup.

The ref-based pattern can be replaced with a functional approach for clarity.

Proposed refactor
       let srcAddress = switch item.srcAddress {
       | Some(addr) => addr
       | None =>
-        // Use first address from contract config
-        let addr = ref(Address.unsafeFromString("0x0000000000000000000000000000000000000000"))
-        chainConfig.contracts->Array.forEach(contract => {
-          if contract.name === contractName {
-            switch contract.addresses->Array.get(0) {
-            | Some(a) => addr := a
-            | None => ()
-            }
-          }
-        })
-        addr.contents
+        chainConfig.contracts
+        ->Array.getBy(contract => contract.name === contractName)
+        ->Option.flatMap(contract => contract.addresses->Array.get(0))
+        ->Option.getWithDefault(Address.unsafeFromString("0x0000000000000000000000000000000000000000"))
       }

Based on learnings: "Use try/catch as expressions instead of refs for tracking success/failure in ReScript"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/envio/src/SimulateItems.res` around lines 93 - 107, The current
ref-based lookup for srcAddress should be replaced with a
functional/option-based lookup: find the matching contract in
chainConfig.contracts (use Array.find or equivalent) by comparing contract.name
to contractName, then map to the first address
(contract.addresses->Array.get(0)) and use that Option result (or a default
Address.unsafeFromString("0x00...")) in the srcAddress binding instead of
mutating a ref; update the srcAddress switch to directly use the resolved Option
so you no longer need the temporary ref or imperative loop.
packages/envio/src/TestIndexer.res (1)

564-575: Add explicit type annotation for Utils.magic cast.

The BigInt-safe JSON serialization approach is sound. However, line 572 uses Utils.magic without an explicit type annotation, which violates the coding guidelines.

Proposed fix
-            "processConfig": processConfig->Utils.magic->jsonStringifyBigIntSafe->Js.Json.parseExn,
+            "processConfig": (processConfig->(Utils.magic: 'a => 'b))->jsonStringifyBigIntSafe->Js.Json.parseExn,

As per coding guidelines: "When using Utils.magic for type casting in ReScript, always add explicit type annotations: value->(Utils.magic: inputType => outputType)"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/envio/src/TestIndexer.res` around lines 564 - 575, The code uses
Utils.magic without an explicit type annotation when constructing workerDataObj
(fields processConfig and initialState); update both casts to include explicit
input/output types per guidelines, e.g., apply Utils.magic with a concrete
annotation on processConfig->Utils.magic and initialState->Utils.magic so the
compiler and readers know the expected types used by jsonStringifyBigIntSafe and
Js.Json.parseExn; locate the uses in workerDataObj and replace bare Utils.magic
with (Utils.magic: YourInputType => YourOutputType) for each field (use the
actual types for processConfig and initialState from their declarations).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@packages/envio/src/SimulateItems.res`:
- Around line 26-42: findEventConfig currently scans
config.chainMap->ChainMap.values (all chains) and uses a ref; change it to
accept ~chainConfig: ChainConfig.t and search only chainConfig.contracts for the
matching contract and event, returning an option directly (no refs). Replace the
nested ref logic with a functional search (e.g., Array.find/keepMap or chained
Array.find on contracts then events) inside findEventConfig and update its call
sites that pass ~config to pass ~chainConfig instead (the call that currently
invokes findEventConfig with ~config should pass ~chainConfig). Ensure the
function signature and callers are updated and remove the ref and imperative
assignment in favor of returning Some(eventConfig) or None.
- Around line 44-55: The parse function uses chainConfig.startBlock and a single
global currentLogIndex, causing items without a number to get the wrong base
block and logIndex not resetting per block; change initialization to derive the
starting block from simulateItems (find the first item with a `number` and use
that as startBlock, falling back to chainConfig.startBlock) and replace the
single global currentLogIndex with per-block logic: keep currentBlock as a ref
and before assigning a logIndex check the item's block number (or the
currentBlock if item has none) and if it differs from currentBlock reset the log
index to 0 and update currentBlock (use these symbols: parse, chainConfig,
simulateItems, currentBlock, currentLogIndex) so each block's logIndex restarts
at 0 and items without `number` use the intended start block.
- Around line 109-121: The issue is that when building `block` and `transaction`
you currently replace the entire default with `item.block`/`item.transaction`,
dropping required defaults like `blockNumber`; instead, merge the provided
override onto the default object so defaults remain. Concretely, for `block`
build a default object (e.g., {"number": blockNumber, "timestamp": 0}) and
shallow-merge properties from `item.block` on top of it before casting to
`Internal.eventBlock`; do the same for `transaction` by merging
`item.transaction` onto the empty/default transaction object prior to casting to
`Internal.eventTransaction`. Ensure merging preserves unspecified default fields
like `number`.

---

Nitpick comments:
In `@packages/envio/src/SimulateItems.res`:
- Around line 93-107: The current ref-based lookup for srcAddress should be
replaced with a functional/option-based lookup: find the matching contract in
chainConfig.contracts (use Array.find or equivalent) by comparing contract.name
to contractName, then map to the first address
(contract.addresses->Array.get(0)) and use that Option result (or a default
Address.unsafeFromString("0x00...")) in the srcAddress binding instead of
mutating a ref; update the srcAddress switch to directly use the resolved Option
so you no longer need the temporary ref or imperative loop.

In `@packages/envio/src/TestIndexer.res`:
- Around line 564-575: The code uses Utils.magic without an explicit type
annotation when constructing workerDataObj (fields processConfig and
initialState); update both casts to include explicit input/output types per
guidelines, e.g., apply Utils.magic with a concrete annotation on
processConfig->Utils.magic and initialState->Utils.magic so the compiler and
readers know the expected types used by jsonStringifyBigIntSafe and
Js.Json.parseExn; locate the uses in workerDataObj and replace bare Utils.magic
with (Utils.magic: YourInputType => YourOutputType) for each field (use the
actual types for processConfig and initialState from their declarations).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4d730159-b556-47f9-ab9b-92030a3e4eea

📥 Commits

Reviewing files that changed from the base of the PR and between 4a10aa4 and e59ceda.

📒 Files selected for processing (4)
  • packages/envio/index.d.ts
  • packages/envio/src/Main.res
  • packages/envio/src/SimulateItems.res
  • packages/envio/src/TestIndexer.res
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/envio/src/Main.res

…e API

- Remove TestHelpers_MockDb.res.hbs template entirely
- Simplify TestHelpers.res.hbs to only export Addresses
- Update index.d.ts.hbs to remove MockDb exports
- Convert all MockDb tests to use createTestIndexer + simulate
- Update template test files (greeter, erc20, fuel, contract import)
- Fix SimulateItems.parse to use undefined for events with no params

https://claude.ai/code/session_012sjgbBChpXi5yVFE8byZN4

@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: 6

🧹 Nitpick comments (2)
packages/cli/templates/dynamic/contract_import_templates/typescript/src/indexer.test.ts.hbs (1)

35-41: Presence-only assertions are too weak for the generated handler test.

toBeDefined() only proves that something was written. Since the template already imports the entity type, compare against a typed expected entity so field-mapping regressions are caught as well.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/cli/templates/dynamic/contract_import_templates/typescript/src/indexer.test.ts.hbs`
around lines 35 - 41, The test's presence-only assertion using toBeDefined() is
too weak; replace it by constructing a typed expected entity (using the imported
entity type, e.g., the same type as
actual{{contract.name.capitalized}}{{event.name}}) and assert field-level
equality against the indexer result from
indexer.{{contract.name.capitalized}}_{{event.name}}.get({{event.entity_id_from_event_code}});
for example, create a const expected: <EntityType> = { ...mapped fields from the
test event } and use
expect(actual{{contract.name.capitalized}}{{event.name}}).toEqual(expected) or
expect(...).toMatchObject(expected) to catch field-mapping regressions.
scenarios/test_codegen/test/EventHandler.test.ts (1)

1032-1036: Consider consolidating assertions and verifying persisted entities.

The multiple individual asserts could be consolidated. Additionally, unlike the first simulate test, this one doesn't verify the actual entities are accessible via indexer.Gravatar.get() after processing.

♻️ Suggested improvement
     // Should have processed both events
-    assert.strictEqual(result.changes.length, 1);
-    assert.strictEqual(result.changes[0]!.eventsProcessed, 2);
-    assert.strictEqual(result.changes[0]!.Gravatar?.sets?.length, 2);
+    assert.deepEqual(
+      { length: result.changes.length, eventsProcessed: result.changes[0]?.eventsProcessed, setsLength: result.changes[0]?.Gravatar?.sets?.length },
+      { length: 1, eventsProcessed: 2, setsLength: 2 }
+    );
+
+    // Verify entities are accessible after processing
+    const gravatar1 = await indexer.Gravatar.get("1");
+    const gravatar2 = await indexer.Gravatar.get("2");
+    assert.strictEqual(gravatar1?.displayName, "First");
+    assert.strictEqual(gravatar2?.displayName, "Second");

Based on learnings: "Always use single assert to check the whole value instead of multiple asserts for every field"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scenarios/test_codegen/test/EventHandler.test.ts` around lines 1032 - 1036,
Consolidate the multiple assertions on the processing outcome into a single deep
assertion (e.g., assert.deepStrictEqual) comparing the whole result.changes[0]
object (including eventsProcessed and Gravatar.sets length) to the expected
shape, and then add a follow-up check that the processed entities are actually
persisted by calling indexer.Gravatar.get() for the expected IDs and asserting
those returned entities match the expected Gravatar records; locate the
assertions around the test’s result variable and the indexer.Gravatar.get()
calls to implement this in the same test block.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@packages/cli/templates/dynamic/contract_import_templates/rescript/src/Indexer_test.res.hbs`:
- Around line 6-18: The generated test Indexer_test.res.hbs currently calls
Indexer.Generated.createTestIndexer() and then indexer.process(...) with only
chains/startBlock/endBlock, which can trigger real I/O and be a no-op; update
the scaffold to pass a simulate array to indexer.process with an in-memory
simulated block/event (e.g., include simulate: [ { chain: "chain1", blockNumber:
X, events: [ { name: "{{event.name}}", params: { ... } } ] } ]) and add at least
one assertion after processing to verify the event handling (use the test
indexer's inspection helpers or generated storage accessors). Ensure you also
set an explicit chain identifier and realistic startBlock/endBlock values in the
process call so the test is deterministic and exercises the simulate path rather
than external I/O.

In
`@packages/cli/templates/dynamic/contract_import_templates/typescript/src/indexer.test.ts.hbs`:
- Around line 14-29: The scaffolded simulate block in the indexer.process call
renders an empty params object which will fail schema validation for
parameterized events; update the template that generates the simulate item (the
simulate entry for contract "{{contract.name.capitalized}}" and event
"{{event.name}}") to emit either a runnable sample payload for each event param
(use the event.params array and each param.js_name to provide sensible
type-based defaults like 0/""/false/"0x..." as appropriate) or omit the entire
params object only when event.params is empty—ensure this change targets the
template code that iterates over event.params so generated tests include valid
params for parameterized events.

In `@scenarios/test_codegen/test/CustomSelection_test.res`:
- Around line 6-22: Replace the simulated "EmptyEvent" in the processConfig
simulate array with the selection-bearing event used by the CustomSelection path
so indexer.process(processConfig) exercises the selected-field shape (i.e.,
change the "event" value from "EmptyEvent" to the event name that triggers the
CustomSelection handler), ensuring processConfig and the indexer.process call
remain otherwise unchanged so the compile-time custom field selection path is
executed during the test.
- Around line 8-21: Replace the unannotated cast using Utils.magic on the
processConfig object with an explicit cast of the form ->(Utils.magic: inputType
=> Indexer.testIndexerProcessConfig); here update the expression that currently
reads ->Utils.magic to ->(Utils.magic: { chains: { [chainId: string]: {
startBlock: int, endBlock: int, simulate: array<{ contract: string, event:
string }> } } } => Indexer.testIndexerProcessConfig) so the inputType exactly
matches the shape of the literal assigned to processConfig and the target type
remains Indexer.testIndexerProcessConfig; adjust types (int vs number) to match
your language's primitive if needed.

In `@scenarios/test_codegen/test/CustomSelection.test.ts`:
- Around line 8-26: The test currently simulates "EmptyEvent" and never asserts
the custom selection; change the simulated event in the indexer.process call to
"CustomSelection" (or the actual handler name that contains selection fields) so
the custom-selection code path runs, then after await indexer.process(...) add a
single assertion that checks the entire produced result object in one go (not
multiple field-by-field asserts) to ensure custom field selection is exercised
and detected; refer to indexer.process and the CustomSelection handler name when
making this change.

In `@scenarios/test_codegen/test/LoadLinkedEntities.res`:
- Around line 57-76: The code uses Utils.magic without an explicit type
annotation when casting the processConfig; update the call so the cast is
explicitly typed (annotate Utils.magic with the correct input/output types) so
processConfig is typed as Indexer.testIndexerProcessConfig before calling
indexer.process, e.g. add a type cast on the value->Utils.magic usage
referencing Indexer.testIndexerProcessConfig to satisfy the
linter/type-guideline.

---

Nitpick comments:
In
`@packages/cli/templates/dynamic/contract_import_templates/typescript/src/indexer.test.ts.hbs`:
- Around line 35-41: The test's presence-only assertion using toBeDefined() is
too weak; replace it by constructing a typed expected entity (using the imported
entity type, e.g., the same type as
actual{{contract.name.capitalized}}{{event.name}}) and assert field-level
equality against the indexer result from
indexer.{{contract.name.capitalized}}_{{event.name}}.get({{event.entity_id_from_event_code}});
for example, create a const expected: <EntityType> = { ...mapped fields from the
test event } and use
expect(actual{{contract.name.capitalized}}{{event.name}}).toEqual(expected) or
expect(...).toMatchObject(expected) to catch field-mapping regressions.

In `@scenarios/test_codegen/test/EventHandler.test.ts`:
- Around line 1032-1036: Consolidate the multiple assertions on the processing
outcome into a single deep assertion (e.g., assert.deepStrictEqual) comparing
the whole result.changes[0] object (including eventsProcessed and Gravatar.sets
length) to the expected shape, and then add a follow-up check that the processed
entities are actually persisted by calling indexer.Gravatar.get() for the
expected IDs and asserting those returned entities match the expected Gravatar
records; locate the assertions around the test’s result variable and the
indexer.Gravatar.get() calls to implement this in the same test block.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3cd221db-dcdd-4a07-9617-ec9679ce540d

📥 Commits

Reviewing files that changed from the base of the PR and between e59ceda and 6d72eab.

📒 Files selected for processing (14)
  • packages/cli/templates/dynamic/codegen/index.d.ts.hbs
  • packages/cli/templates/dynamic/codegen/src/TestHelpers.res.hbs
  • packages/cli/templates/dynamic/codegen/src/TestHelpers_MockDb.res.hbs
  • packages/cli/templates/dynamic/contract_import_templates/rescript/src/Indexer_test.res.hbs
  • packages/cli/templates/dynamic/contract_import_templates/typescript/src/indexer.test.ts.hbs
  • packages/cli/templates/static/blank_template/rescript/src/Indexer_test.res
  • packages/cli/templates/static/erc20_template/typescript/src/indexer.test.ts
  • packages/cli/templates/static/greeter_template/typescript/src/indexer.test.ts
  • packages/cli/templates/static/greeteronfuel_template/typescript/src/indexer.test.ts
  • packages/envio/src/SimulateItems.res
  • scenarios/test_codegen/test/CustomSelection.test.ts
  • scenarios/test_codegen/test/CustomSelection_test.res
  • scenarios/test_codegen/test/EventHandler.test.ts
  • scenarios/test_codegen/test/LoadLinkedEntities.res
💤 Files with no reviewable changes (3)
  • packages/cli/templates/static/blank_template/rescript/src/Indexer_test.res
  • packages/cli/templates/dynamic/codegen/src/TestHelpers_MockDb.res.hbs
  • packages/cli/templates/dynamic/codegen/src/TestHelpers.res.hbs
✅ Files skipped from review due to trivial changes (1)
  • packages/envio/src/SimulateItems.res

Comment on lines 6 to 18
Async.it("{{contract.name.capitalized}}_{{event.name}} is created correctly", async _t => {
let indexer = Indexer.Generated.createTestIndexer()

// Creating mock for {{contract.name.capitalized}} contract {{event.name}} event
let event = {{event.create_mock_code}};

Async.it("{{contract.name.capitalized}}_{{event.name}} is created correctly", async t => {
// Processing the event
let mockDbUpdated = await {{contract.name.capitalized}}.{{event.name}}.processEvent({
event,
mockDb,
// TODO: Configure chain ID, start/end blocks, and event params for your contract
let _ = await indexer.process({
chains: {
chain1: Some({
startBlock: 0,
endBlock: 100,
}),
},
})

// Getting the actual entity from the mock database
let actual{{contract.name.capitalized}}{{event.name}} =
mockDbUpdated.entities.{{contract.name.uncapitalized}}_{{event.name}}.get(
{{event.entity_id_from_event_code}},
)->Option.getExn

// Creating the expected entity
let expected{{contract.name.capitalized}}{{event.name}}: Indexer.{{contract.name.uncapitalized}}_{{event.name}} = {
id: {{event.entity_id_from_event_code}},
{{#each event.params as |param|}}
{{param.res_name}}: event.params.{{param.res_name}}{{#if param.is_eth_address}}->Address.toString{{/if}},
{{/each}}
}
//Assert the expected {{contract.name.capitalized}} {{event.name}} entity
t.expect(
actual{{contract.name.capitalized}}{{event.name}},
~message="Actual {{contract.name.capitalized}}_{{event.name}} should be the same as the expected {{contract.name.capitalized}}_{{event.name}}",
).toEqual(expected{{contract.name.capitalized}}{{event.name}})
})

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.

⚠️ Potential issue | 🟠 Major

The generated ReScript test is no longer deterministic.

This scaffold calls indexer.process with only a block range and no simulate items, so generated tests can still fall back to real source I/O instead of the new in-memory simulate path. Because nothing is asserted afterward, a no-op run would also pass.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/cli/templates/dynamic/contract_import_templates/rescript/src/Indexer_test.res.hbs`
around lines 6 - 18, The generated test Indexer_test.res.hbs currently calls
Indexer.Generated.createTestIndexer() and then indexer.process(...) with only
chains/startBlock/endBlock, which can trigger real I/O and be a no-op; update
the scaffold to pass a simulate array to indexer.process with an in-memory
simulated block/event (e.g., include simulate: [ { chain: "chain1", blockNumber:
X, events: [ { name: "{{event.name}}", params: { ... } } ] } ]) and add at least
one assertion after processing to verify the event handling (use the test
indexer's inspection helpers or generated storage accessors). Ensure you also
set an explicit chain identifier and realistic startBlock/endBlock values in the
process call so the test is deterministic and exercises the simulate path rather
than external I/O.

Comment on lines +14 to +29
// TODO: Configure chain ID, start/end blocks, and event params for your contract
await indexer.process({
chains: {
1: {
startBlock: 0,
endBlock: 100,
simulate: [
{
contract: "{{contract.name.capitalized}}",
event: "{{event.name}}",
params: {
{{#each event.params as |param|}}
// {{param.js_name}}: ...,
{{/each}}
},
},

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.

⚠️ Potential issue | 🟠 Major

This scaffolded simulate item will fail for parameterized events.

simulate inputs are schema-validated, so rendering an empty params object here means any generated test for an event with required fields will throw before it reaches the assertion. Please emit a runnable sample payload, or omit params entirely only when the event truly has no parameters.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/cli/templates/dynamic/contract_import_templates/typescript/src/indexer.test.ts.hbs`
around lines 14 - 29, The scaffolded simulate block in the indexer.process call
renders an empty params object which will fail schema validation for
parameterized events; update the template that generates the simulate item (the
simulate entry for contract "{{contract.name.capitalized}}" and event
"{{event.name}}") to emit either a runnable sample payload for each event param
(use the event.params array and each param.js_name to provide sensible
type-based defaults like 0/""/false/"0x..." as appropriate) or omit the entire
params object only when event.params is empty—ensure this change targets the
template code that iterates over event.params so generated tests include valid
params for parameterized events.

Comment thread scenarios/test_codegen/test/CustomSelection_test.res Outdated
Comment on lines +8 to +21
let processConfig: Indexer.testIndexerProcessConfig = {
"chains": {
"1337": {
"startBlock": 1,
"endBlock": 100,
"simulate": [
{
"contract": "Gravatar",
"event": "EmptyEvent",
},
],
},
},
})

// Test content of the generated record type
let _ = ((event.transaction: Indexer.Gravatar.CustomSelection.transaction :> expectedTransactionFields) :> Indexer.Gravatar.CustomSelection.transaction)
let _ = ((event.block: Indexer.Gravatar.CustomSelection.block :> expectedBlockFields) :> Indexer.Gravatar.CustomSelection.block)

// The event not used for the test, but we want to make sure
// that events without custom field selection use the global one
let anotherEvent = Gravatar.EmptyEvent.createMockEvent({})
let _ = ((anotherEvent.transaction: Indexer.Gravatar.EmptyEvent.transaction :> expectedGlobalTransactionFields) :> Indexer.Gravatar.EmptyEvent.transaction)
let _ = ((anotherEvent.block: Indexer.Gravatar.EmptyEvent.block :> expectedGlobalBlockFields) :> Indexer.Gravatar.EmptyEvent.block)

let updatedMockDb = await Gravatar.CustomSelection.processEvent({
event,
mockDb: mockDbInitial,
})

t.expect(updatedMockDb.entities.customSelectionTestPass.get(hash)).not.toBe(None)
}->Utils.magic

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.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n --context 1 '\->Utils\.magic\b' scenarios/test_codegen/test/CustomSelection_test.res

Repository: enviodev/hyperindex

Length of output: 142


Add explicit type annotations to the Utils.magic cast.

Line 21 uses ->Utils.magic without explicit type annotations on the conversion boundary. Per coding guidelines, this must be: ->(Utils.magic: inputType => Indexer.testIndexerProcessConfig). The input type should reflect the object literal being constructed (likely an inferred object type from the fields defined above).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scenarios/test_codegen/test/CustomSelection_test.res` around lines 8 - 21,
Replace the unannotated cast using Utils.magic on the processConfig object with
an explicit cast of the form ->(Utils.magic: inputType =>
Indexer.testIndexerProcessConfig); here update the expression that currently
reads ->Utils.magic to ->(Utils.magic: { chains: { [chainId: string]: {
startBlock: int, endBlock: int, simulate: array<{ contract: string, event:
string }> } } } => Indexer.testIndexerProcessConfig) so the inputType exactly
matches the shape of the literal assigned to processConfig and the target type
remains Indexer.testIndexerProcessConfig; adjust types (int vs number) to match
your language's primitive if needed.

Comment thread scenarios/test_codegen/test/CustomSelection.test.ts Outdated
Comment thread scenarios/test_codegen/test/LoadLinkedEntities.res Outdated
claude and others added 18 commits March 23, 2026 09:35
Resolve conflicts:
- TestHelpers_MockDb.res.hbs: keep our deletion (moved to envio package)
- TestIndexer.res: take main's Utils.magic approach over BigInt JSON serialization

https://claude.ai/code/session_012sjgbBChpXi5yVFE8byZN4
- ReScript template: Indexer.createTestIndexer() (not Generated), remove Some() wrapper
- TS template: extract event variable, restore expected entity comparison with toEqual
- Export Addresses directly from generated package (index.js.hbs, index.d.ts.hbs)
- Update erc20/greeter/greeteronfuel templates to import Addresses directly

https://claude.ai/code/session_012sjgbBChpXi5yVFE8byZN4
…late

- Move TestHelpers_MockAddresses.res from static codegen to envio/src
- Export Addresses directly from envio package (index.js, index.d.ts)
- Update generated index.d.ts.hbs to import Addresses from "envio"
- Remove @Gentype from TestHelpers.res.hbs (no longer needs .gen.ts)
- Add simulate field to TestIndexer.chainConfig type
- Update ReScript contract_import template to use simulate

https://claude.ai/code/session_012sjgbBChpXi5yVFE8byZN4
- Add TestHelpers.Addresses module to Envio.res
- Delete TestHelpers.res.hbs codegen template
- Export TestHelpers from envio package (index.js, index.d.ts)
- Update generated index.js.hbs and index.d.ts.hbs to import from envio

https://claude.ai/code/session_012sjgbBChpXi5yVFE8byZN4
- Generated index.js.hbs/index.d.ts.hbs now only re-export TestHelpers from envio
- Remove standalone Addresses export from envio index.js/index.d.ts
- Update static test templates to destructure Addresses from TestHelpers

https://claude.ai/code/session_012sjgbBChpXi5yVFE8byZN4
- Move Indexer_test.res.hbs and indexer.test.ts.hbs codegen to Rust
  with snapshot tests for EVM and Fuel
- Delete TestHelpers_MockAddresses module, inline into Envio.res
- Remove TestHelpers re-export from index.js (exported via Envio.res.mjs)
- Throw user-friendly error on invalid block range in SimulateSource
  instead of silently filtering
- Prefix SimulateItems types with ecosystem (evmSimulateEventItem,
  fuelSimulateEventItem, etc.)
- Split SimulateEventItemBase into EVM/Fuel variants with correct block
  fields (number vs height) in index.d.ts
- Restore deleted comments in erc20 template

https://claude.ai/code/session_012sjgbBChpXi5yVFE8byZN4
…tor, entity ops, and GADT simulate types

- Add paramsConstructor type and makeSimulateItem function per event in Indexer.res
- Add testIndexerEntityOps type and entity access fields on testIndexer
- Generate GADT event identifier types (per-contract + top-level simulateContractEvent)
- Add simulateBlock helper function
- Update ReScript test file generation to use makeSimulateItem with typed params
- Add default_value_rescript field to Param struct for test codegen
- Update all assertion tests and snapshots

https://claude.ai/code/session_012sjgbBChpXi5yVFE8byZN4
Replace @as("X") camelX: pattern with quoted key "X": pattern for chain
numeric ID fields and entity fields in handlerContext/testIndexer types,
matching the existing contract field codegen style.

https://claude.ai/code/session_012sjgbBChpXi5yVFE8byZN4
- Rename entityHandlerContext -> handlerEntityOperations
- Rename testIndexerEntityOps -> testIndexerEntityOperations
- Fix simulate event GADT types to use lowercase names (ReScript requirement)
- Fix simulateBlock optional parameter type
- Use quoted keys for testIndexerProcessConfigChains fields
- Use quoted key in contract_import test template
- Update all scenario files for quoted field access syntax

https://claude.ai/code/session_012sjgbBChpXi5yVFE8byZN4
- Generate contract modules (with event sub-modules) in Rust codegen
  as part of project_template.indexer_code instead of handlebars template
- Inline MakeRegister functor code (handler + contractRegister bindings)
  directly into each event module
- Remove MakeRegister functor, module type Event, and contract/event
  loop from Indexer.res.hbs template
- Update snapshot tests for new indexer_code structure

https://claude.ai/code/session_012sjgbBChpXi5yVFE8byZN4
These values are already present in internal.config.json and parsed by
Config.fromPublic() at runtime. The inline copies in the generated
contract modules were redundant. Fuel contracts keep their abi since
event modules reference it for decoding.

https://claude.ai/code/session_012sjgbBChpXi5yVFE8byZN4
DZakh and others added 19 commits March 30, 2026 14:08
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix ReScript contract import template to use quoted PascalCase for
  entity context access (e.g. context."FiatTokenProxy_AdminChanged")
- Use S.reverseConvertOrThrow for simulate params validation (fixes
  BigInt parsing failure for simulate events)
- Respect user-provided block numbers in simulate items instead of
  always using auto-incremented values
- Use block timestamp from parsed block instead of hardcoding 0
- Move evmSimulateEventItem type to Envio.res, remove duplicate
  simulateItem type, prefix parse functions with Evm
- Add SimulateTestEvent entity and tests for block/logIndex management

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Use \\\" in Rust format string to produce \" in generated ReScript
  (context.\"Entity_Event\".set)
- Add getAll method to EntityOps TypeScript type for test indexer

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
context.\"FiatTokenProxy_AdminChanged".set(entity)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The source .res file was deleted but the generated .gen.ts was left behind,
causing TS2307 module not found error.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace old MockDb/processEvent pattern with the new createTestIndexer
and simulate API, matching the updated fuel greeter template.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add fuelSimulateEventItem type (params required vs EVM optional)
- Add fuelChainConfig with simulate field to TestIndexer
- Add Fuel block/transaction schemas (height/time/id fields)
- Update SimulateItems.parse to handle Fuel ecosystem
- Update codegen to use fuelChainConfig for Fuel chains
- Update TypeScript types: FuelTestIndexerChainConfig with simulate

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The source manager may call getItemsOrThrow multiple times with
different partition IDs. Return all items on the first call and
empty on subsequent calls to prevent duplicate event processing.

Also apply startBlock from processConfig to chainConfig in patchConfig.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Greeter template test: use hardcoded chain ID 137 matching config
- Contract import codegen: use startBlock/endBlock 1 instead of 0
- Update entity_id format to match new block numbering (chainId_1_0)
- Use S.convertOrThrow for simulate params (handles both BigInt and
  Address conversion correctly)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The engine treats knownHeight=0 as "no blocks available" (FetchState
getQueryPlan returns WaitingForNewBlock when headBlockNumber<=0).
SimulateSource now reports height as max(endBlock, 1) to work around
this. Also set endBlock on chainConfig in patchConfig so the engine
knows when to exit.

Greeter template test updated with correct chain ID 137 and matching
startBlock from config.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…1066)

These @Gentype attributes were only needed by the old testing framework
which has been removed. Also removes dead Enum.gen re-export from
index.d.ts.hbs (no Enum.res file exists).

https://claude.ai/code/session_015zCdS7e7RYRnS4jwnpamkh

Co-authored-by: Claude <noreply@anthropic.com>
…ev/hyperindex into claude/add-indexer-test-api-XcpxD
* Update GitHub Actions to Node.js 24-compatible versions

actions/checkout v4→v6, actions/setup-node v4→v6,
actions/upload-artifact v4→v7, actions/download-artifact v4→v8,
pnpm/action-setup v4→v5.

https://claude.ai/code/session_01TV3JWeXCsyNysFz9phMeKG

* Set FORCE_JAVASCRIPT_ACTIONS_TO_NODE24 in all workflows

Forces remaining third-party actions (rust-toolchain, rust-cache, etc.)
to also run on Node.js 24.

https://claude.ai/code/session_01TV3JWeXCsyNysFz9phMeKG

* Replace Swatinem/rust-cache and cache-apt-pkgs with actions/cache@v5

Swatinem/rust-cache@v2 internally uses actions/cache/restore@v4 (node20),
and awalsh128/cache-apt-pkgs-action@v1 uses actions/cache/save@v4.
Replace both with actions/cache@v5 (node24) and direct apt-get install.

https://claude.ai/code/session_01TV3JWeXCsyNysFz9phMeKG

* Extract cargo-cache composite action to deduplicate cache config

https://claude.ai/code/session_01TV3JWeXCsyNysFz9phMeKG

---------

Co-authored-by: Claude <noreply@anthropic.com>
In CI, template tests install the envio package via the file: path returned
by get_envio_version(). packages/envio is source-only (compiled .res.mjs
files are gitignored), so pnpm copies source to its store without them,
causing "BigInt.res.mjs not found" at runtime.

When .envio-artifacts/envio exists (pre-built in CI), prefer it — it
includes the compiled files needed to run the indexer.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…artifacts/

The previous fix unconditionally preferred .envio-artifacts/envio over
packages/envio when walking up from the binary. This broke scenarios-test:
the dev binary (target/debug/envio) picked .envio-artifacts/envio while
test_codegen/package.json hardcodes packages/envio, creating two different
envio module instances that both register the same Prometheus metrics.

Now we only prefer the artifact when the binary itself lives inside
.envio-artifacts/ (i.e. the pre-built CI artifact used by template-tests).
Dev binaries in target/ continue to use packages/envio, avoiding duplication.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… restore comment

- Assert `result.changes[0]?.addresses` (whole object) instead of drilling
  into `?.addresses?.sets` — catches unexpected extra fields per CLAUDE.md
  "single assert on whole value" principle
- Use vitest t.expect for the unawaited macrotask test (binds to test context)
- Restore the commented-out log assertion with explanation that there's
  currently no good way to test it — preserves useful context for future

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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