Skip to content

Add local-runner for extraction testing and improve test loading#74

Open
gasperzgonec wants to merge 9 commits intomainfrom
offline_testing
Open

Add local-runner for extraction testing and improve test loading#74
gasperzgonec wants to merge 9 commits intomainfrom
offline_testing

Conversation

@gasperzgonec
Copy link
Copy Markdown
Contributor

@gasperzgonec gasperzgonec commented Mar 19, 2026

Fixture-based local testing with MockServer

Replaces the old test runner (which required real DevRev credentials) with a fixture-based system that uses the SDK's MockServer. Snap-in functions can now be tested locally without any external dependencies.

Changes

code/src/test-runner/test-runner.ts -- Full rewrite

  • Starts a MockServer (from @devrev/ts-adaas) on a dynamic port
  • Loads state.json from the fixture directory and injects it via the GET /worker_data_url.get route override
  • Loads event_context.json and builds a complete AirdropEvent with all SDK URLs (callback_url, worker_data_url, devrev_endpoint) pointing at the MockServer
  • All other DevRev API endpoints (artifact upload, snap-ins.get, domain-mappings.install, etc.) return default 200 OK responses automatically
  • Supports ${ENV_VAR} templating in fixture JSON files -- variables are resolved from process.env (with .env auto-loaded via dotenv). Missing variables produce a clear error message with the variable name and file path.
  • Removed the old addCredentials / dotenv-PAT injection flow

code/src/main.ts -- Updated CLI interface

  • --fixturePath (required): name of the fixture folder inside code/fixtures/
  • --functionName (optional): which function to run. Can also be set via function_name in event_context.json
  • --local (optional): enables the SDK's local development mode -- log output is plain text messages instead of full JSON objects with event context tags

code/package.json

  • Added "fixture" npm script

code/fixtures/ -- Example fixtures

  • start_extracting_external_sync_units/ -- event_context.json + state.json
  • start_extracting_data/ -- event_context.json + state.json

Usage

# Run a fixture (JSON log output):
npm run fixture -- --fixturePath start_extracting_external_sync_units

# Run with plain-text logs:
npm run fixture -- --fixturePath start_extracting_external_sync_units --local

# Override function name from CLI:
npm run fixture -- --fixturePath start_extracting_data --functionName extraction --local

Creating new fixtures

Create a folder in code/fixtures/<name>/ with two files:

event_context.json (only event_type is required):

{
  "event_type": "START_EXTRACTING_DATA",
  "function_name": "extraction",
  "connection_data": {
    "key": "${EXTERNAL_SYSTEM_TOKEN}",
    "key_type": "pat"
  }
}

state.json (optional -- defaults to empty state if missing):

{
  "todos": { "completed": false },
  "users": { "completed": false },
  "attachments": { "completed": false }
}

Environment variables can be provided via a .env file or exported in your shell.

How it works

  1. dotenv.config() loads .env into process.env
  2. Fixture JSON files are read and ${VAR} placeholders are resolved
  3. MockServer starts on a random available port
  4. state.json content is injected into the mock GET /worker_data_url.get endpoint
  5. A full AirdropEvent is constructed with test defaults and MockServer URLs
  6. The snap-in function is called with the event
  7. MockServer is stopped after completion

No real credentials or network access to DevRev is needed. External system calls (e.g. the HttpClient in the template) still hit their configured endpoints as normal.

Connected Issues

Checklist

  • Tests added/updated and ran with npm run test OR no tests needed.
  • Code formatted and checked with npm run lint.
  • Added "How to test" section to the description OR this section is not needed.

Copy link
Copy Markdown
Contributor

@radovanjorgic radovanjorgic left a comment

Choose a reason for hiding this comment

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

I quickly reviewed and gave some comments, but I don't think this is the right direction. Approach is good (mock server, fixtures) but adding so much code in template is not what we want. We should think of abstracting this in SDK.

@gasperzgonec gasperzgonec marked this pull request as ready for review March 24, 2026 16:54
Copilot AI review requested due to automatic review settings March 24, 2026 16:54
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR replaces the credential-dependent local test runner with a fixture-based runner that uses @devrev/ts-adaas MockServer, enabling snap-in functions to be executed locally without needing real DevRev access.

Changes:

  • Rewrote the test runner to load event_context.json / state.json, resolve ${ENV_VAR} placeholders, and run against a MockServer.
  • Updated the CLI to accept --fixturePath (required) and optional --functionName, --local, and --printState.
  • Added example fixture directories and an npm run fixture script.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
code/src/test-runner/test-runner.ts New fixture-driven runner that builds an AirdropEvent, starts/stops MockServer, injects fixture state, and supports env templating.
code/src/main.ts Updates CLI args for fixture execution and injects positional local for SDK spawn local-mode logging.
code/package.json Adds fixture script alias to run the fixture CLI via ts-node.
code/fixtures/start_extracting_external_sync_units/event_context.json Example fixture event context for external sync unit extraction start.
code/fixtures/start_extracting_external_sync_units/state.json Example state fixture for extraction.
code/fixtures/start_extracting_data/event_context.json Example fixture event context for START_EXTRACTING_DATA.
code/fixtures/start_extracting_data/state.json Example state fixture for extraction.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
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.

Please verify if fetch state returns exactly this object or full state object (with fields like lastSuccessfulSyncStarted, attachmentsMetadata and so on). We should have the full state, not sure if we should define only initial or full state here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The fetch state returns a full object, containing all of these values, if present.

Copy link
Copy Markdown
Contributor

@radovanjorgic radovanjorgic Mar 30, 2026

Choose a reason for hiding this comment

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

So if we have only {"foo": "bar"} in state.json, it will return full state object, with fields like lastSuccessfulSyncStared and attachmentsMetadata? Those fields will be overriden if explicitly specified in state.json file?

@@ -0,0 +1,13 @@
{
"event_type": "START_EXTRACTING_DATA",
"function_name": "extraction",
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.

Hmm, if we name this event/airdrop message it is a bit confusing to have additional fields like function_name here. I would rather keep it as other snap-ins do - that you need to specify through args the function you want to test offline.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

While this is a fair observation, I don't think it's very reproducible. I'd like to have it be stored in the file, as one fixture will be either for extraction or loading, but never both (forcing this to be a CLI arg could add a new problem that could occur when we mismatch and push an extraction event into a loading function, which will never happen in the real life).

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.

I get the point but wanted to have clear separation of concerns here. If I am a developer, I would just copy the content of the real event into event.json and expect to be ready to test offline with it. Without having to add or understand additional fields like function_name. function_name sounds like "how to run" part of the process and is already a familiar dev flow for existing snap-ins (not adaas snapins).

Example from devrev-snaps-typescript-template README.md (https://github.com/devrev/devrev-snaps-typescript-template):

npm install
npm run start -- --functionName=on_work_creation --fixturePath=on_work_created_event.json

"event_context": {
"external_sync_unit_id": "test_external_sync_unit_id"
}
}
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.

Are all of these fields required? Are all required present here? For example I know request id field (or uuid form event context IIRC) is used when doing some requests, but I don't see it here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

None of these fields are required, this file can be empty.
The defaults here override the missing values in there.

* Fields from the fixture's airdrop_message.json are merged into the
* appropriate sections, with MockServer URLs always taking precedence.
*/
function buildEvent(mockServerBaseUrl: string, eventType: EventType, fixture?: FixtureAirdropMessage): AirdropEvent {
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.

We have similar createEvent function in SDK, maybe we can export and use it here as well?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

SDK's createEvent is coupled to its internal Jest MockServer singleton — can't inject a custom base URL. Filed as future SDK enhancement.

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.

I would rather delay this and add what is needed to SDK to deliver simplified version of this, then: add to template, announce new feature -> people start pulling the same code, add to sdk -> tell people that this is outdated and can be removed.

console.log(`[fixture] MockServer started on ${mockServer.baseUrl}`);

// Inject state from state.json (or default to empty state)
mockServer.setRoute({
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.

For now we can go with state only, but is there something else that user would like to override to test locally?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Maybe we could add a different configuration options, for example, to support custom responses for some other endpoints (e.g: selective extraction itemTypes).

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.

Yes, we could do that.

*/
export interface FixtureEvent {
event_type: string;
function_name?: FunctionFactoryType;
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.

If we keep function_name (which I would not suggest), this will be required parameter, right?

* Replaces `${VAR_NAME}` placeholders with values from `process.env`.
* Values are JSON-escaped so special characters don't break the JSON structure.
*/
function resolveEnvVariables(raw: string, filePath: string): string {
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.

Oh okay so this enables using env variables in fixtures json files like this for example?

{
    "foo": "${ENV_VAR_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.

Where .env file should be located? In code/?

throw new Error(`Unknown event_type "${input}". Must be one of: ${Object.values(EventType).join(', ')}`);
}

function buildEvent(mockServerBaseUrl: string, eventType: EventType, fixture?: FixtureEvent): AirdropEvent {
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.

As said above, if possible let's move this to the sdk and reuse from there (if that is not harmful) and keep template minimal.

console.log(`[fixture] MockServer started on ${mockServer.baseUrl}`);

// Inject state from state.json (or default to empty state)
mockServer.setRoute({
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.

Yes, we could do that.

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.

There is no state yet in start external sync units phase. It is stateless.

/** Which function to run — overrides airdrop_message.json's function_name if set */
functionName?: FunctionFactoryType;
/** Print the adapter state on every worker_data_url.update call */
printState?: boolean;
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.

Okay maybe saving to the file is not needed for now (since we don't have any orchestration) but then I think even --printState is not needed as flag. Anyone can just log the adapter.state at the end of process to see what the state looks like. And we anyhow have State is successfully updated to {...state} log from SDK at the end of extraction.

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.

3 participants