Skip to content

[v3] [Test] effect not mocked #1200

Description

@vibern0

Describe the bug
We previously used fetch-mock to fetch external API requests. This was enough to mock effects.
But has of v3, effects do not run on the main thread and the mock does not work.

To Reproduce
Create a handler with an effect calling an external API with data that will fail (on purpose, this are tests, we don't use real data). Write a test testing that handler and see it failing.

Expected behavior
It should be possible to mock the effect with specific input and specific output.

Local (please complete the following information):

  • Envio version: 3.0.0-rc.0
  • Node version: 24
  • pnpm version: 11
  • Docker version (if running locally): 4.60.1

Additional context
Claude suggested a patch, plus some extra code.

patch

diff --git a/src/TestIndexer.res.mjs b/src/TestIndexer.res.mjs
index ba65ab0c2f635afcaae61795ffe1ac34cb1750dc..9fde800c1c1cec8c9d672bf9812d5db6fe456a84 100644
--- a/src/TestIndexer.res.mjs
+++ b/src/TestIndexer.res.mjs
@@ -384,7 +384,12 @@ function makeCreateTestIndexer(config, workerPath) {
       progressBlockByChain: {},
       entities: entities,
       entityConfigs: entityConfigs,
-      processChanges: []
+      processChanges: [],
+      /* Effect-cache override. Tests prime this via `__primeEffectCache`
+       * before calling `process()`; on each `runChainWorker` we merge this
+       * into the per-call `initialState.cache` so the worker thread's
+       * LoadLayer triggers the cache lookup path. */
+      cache: {}
     };
     let entityOpsDict = {};
     allEntities.forEach(entityConfig => {
@@ -470,6 +475,42 @@ function makeCreateTestIndexer(config, workerPath) {
     Object.entries(entityOpsDict).forEach(param => {
       result[param[0]] = param[1];
     });
+    /* TEST-ONLY: pre-populate the in-memory effect cache so effect handlers
+     * don't fire (and therefore don't try to `fetch` from inside the Worker
+     * thread, where main-thread mocks can't reach). Pass each entry as
+     * `{input, output}` — the helper computes the cacheKey the same way
+     * `UserContext.callEffect` does (Utils.Hash.makeOrThrow of the input).
+     * The `output` JSON must conform to the effect's declared output schema. */
+    result["__primeEffectCache"] = (effectName, entries) => {
+      let tableName = "envio_effect_" + effectName;
+      if (state.entityConfigs[tableName] === undefined) {
+        state.entityConfigs[tableName] = {
+          name: tableName,
+          table: { tableName: tableName },
+          /* loose JSON schema — handleLoadByIds passes the entity through
+           * `reverseConvertToJsonOrThrow(entity, schema)`; the real schema
+           * validation happens later in LoadLayer against the effect's
+           * outputSchema. */
+          schema: S$RescriptSchema.json(false)
+        };
+      }
+      if (state.entities[tableName] === undefined) {
+        state.entities[tableName] = {};
+      }
+      let dict = state.entities[tableName];
+      entries.forEach(entry => {
+        let id = Utils.Hash.makeOrThrow(entry.input);
+        dict[id] = { id: id, output: entry.output };
+      });
+      /* The LoadLayer's cache lookup is gated on Utils.Dict.has(cache, effectName).
+       * Initialize a non-empty record so the lookup path runs. */
+      let existing = state.cache[effectName];
+      if (existing === undefined) {
+        state.cache[effectName] = { effectName: effectName, count: entries.length };
+      } else {
+        existing.count = existing.count + entries.length | 0;
+      }
+    };
     result["process"] = processConfig => {
       if (state.processInProgress) {
         Stdlib_JsError.throwWithMessage("createTestIndexer process is already running. Only one process call is allowed at a time");
@@ -533,6 +574,9 @@ function makeCreateTestIndexer(config, workerPath) {
           });
         }
         let initialState = makeInitialState(config, chains, indexingAddressesByChain);
+        /* Merge test-primed effect cache so the worker's LoadLayer cache
+         * lookup is triggered for effects we pre-seeded. */
+        Object.assign(initialState.cache, state.cache);
         return new Promise((resolve, reject) => {
           let workerData_startBlock = processChainConfig.startBlock;
           let workerData_endBlock = processChainConfig.endBlock;

extra code

/**
 * Backed by the local envio patch (`patches/envio@3.0.0-rc.0.patch`) which
 * exposes `__primeEffectCache` on the test indexer. The patch computes the
 * same `Utils.Hash.makeOrThrow(input)` cacheKey envio uses internally.
 */
export const primeEffectCache = <Input, Output>(
  indexer: TestIndexer,
  effectName: string,
  entries: ReadonlyArray<{ input: Input; output: Output }>,
) => {
  const fn = (indexer as unknown as Record<string, unknown>)[
    "__primeEffectCache"
  ];
  if (typeof fn !== "function") {
    throw new Error(
      "indexer.__primeEffectCache is missing. Ensure patches/envio@3.0.0-rc.0.patch is applied via pnpm-workspace.yaml's patchedDependencies.",
    );
  }
  (fn as (n: string, e: ReadonlyArray<unknown>) => void)(effectName, entries);
};

then specific mocks

/**
 * Convenience wrapper around `primeEffectCache` for the cow.fi
 * `fetchOrderDetailsData` effect — used by `GPv2Settlement.PreSignature`
 * (and indirectly the Trade handler). Matches the shape of the cow.fi
 * `/orders/{uid}` mock these tests register with `fetchMock.get`.
 */
export const primeCowOrder = (
  indexer: TestIndexer,
  params: {
    orderUid: string;
    sellToken: string;
    buyToken: string;
    sellAmount: bigint | string;
    buyAmount: bigint | string;
    receiver: string;
    feeAmount?: bigint | string;
    validTo?: number;
    partiallyFillable?: boolean;
    appData?: string;
  },
) => {
  primeEffectCache(indexer, "fetchOrderDetailsData", [
    {
      input: params.orderUid,
      output: {
        sellToken: params.sellToken,
        buyToken: params.buyToken,
        sellAmount: params.sellAmount.toString(),
        buyAmount: params.buyAmount.toString(),
        feeAmount: (params.feeAmount ?? 0n).toString(),
        validTo: params.validTo ?? Math.floor(Date.now() / 1000) + 3600,
        partiallyFillable: params.partiallyFillable ?? false,
        receiver: params.receiver,
        appData: params.appData ?? "{}",
      },
    },
  ]);
};

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions