Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
c83fda9
record and play test framework outline.
WuonParticle Jul 1, 2025
26fe4f5
rnpExample for testing record and play framework
WuonParticle Jun 26, 2025
015c8d2
Get 2 method mapping on single dependency working as well as mulitple…
WuonParticle Jun 26, 2025
dc93fe2
Add support for having unmocked methods fail.
WuonParticle Jun 26, 2025
dd8e907
fix wallet failure tests which fail when sensible is registered globa…
WuonParticle Jul 1, 2025
89c2956
don't print the logo for unit tests.
WuonParticle Jul 1, 2025
cd94a33
Add allowPassThrough option to enable methods on dependencies which a…
WuonParticle Jun 27, 2025
bdc3448
reset mocks after each test.
WuonParticle Jun 27, 2025
ad6236a
separate out each route into a separate file to match connectors and …
WuonParticle Jun 27, 2025
dad3044
Add prototype dependency support with useProtoDep route and correspon…
WuonParticle Jun 27, 2025
eec08c1
introduce a parameter object for api-test-case to help deal with the …
WuonParticle Jun 27, 2025
eb5c5c7
rename init methods to contain init.
WuonParticle Jun 27, 2025
a3165cf
add useB example to recorder .
WuonParticle Jun 27, 2025
e0235d6
fix harness reset for recorder tests
WuonParticle Jun 27, 2025
a00aa35
rename unmappedDep to unlistedDep for clarity
WuonParticle Jun 27, 2025
8216523
Throw error when mocked method doesn't have a mock loaded
WuonParticle Jun 27, 2025
8d48f73
testing guide
WuonParticle Jun 27, 2025
435b814
refactor: Comments and LLM refactor for clarity.
WuonParticle Jun 27, 2025
e9b7026
throw more detailed error about missing value for spy.
WuonParticle Jun 30, 2025
cee6c96
move save mock after assertStatusCode for consistency with unit tests.
WuonParticle Jun 30, 2025
39d20c0
construct a custom error message to make stack trace come from internals
WuonParticle Jun 30, 2025
979d1f8
move save mock before assertStatusCode to allow moving to unit tests …
WuonParticle Jun 30, 2025
68d1973
handle cases of a dependency being retried.
WuonParticle Jun 30, 2025
b71da5c
add superjson support for custom object deserialization during rnp.
WuonParticle Jul 1, 2025
c7faa60
join error thrown when saving mocks into one error.
WuonParticle Jul 2, 2025
646f1c9
PR comment about clarity with proto property
WuonParticle Jul 2, 2025
f9a104c
switch to templates instead of string concatenation.
WuonParticle Jul 2, 2025
38a15a8
update rnp snapshots
WuonParticle Jul 2, 2025
e39fa26
make possible to mock non async method in the future.
WuonParticle Jul 2, 2025
c7a78af
create a guide for setting up the project testing and debugging in vs…
WuonParticle Jul 9, 2025
a5e797c
make method selector a lambda instead of a string.
WuonParticle Jul 18, 2025
f797d0a
Change rnpExample method names to be more descriptive
WuonParticle Jul 18, 2025
579caf1
split use unloaded method test case into Recorder and Mocked cases to…
WuonParticle Jul 18, 2025
5d13ecb
Move unlistedDep test, delete Recorder tests and redo some comments +…
WuonParticle Jul 18, 2025
4e9525a
create helper to streamline test creation.
WuonParticle Jul 18, 2025
4f7b5e2
make TestDependencyContract more type safe by adding TObject to signa…
WuonParticle Jul 18, 2025
87b0dc2
use `call` instead of `use` when referring to usage of dependency met…
WuonParticle Jul 18, 2025
aaab4f2
update description on cursor rule
WuonParticle Jul 18, 2025
79b54b1
use 501 instead of 424 status code on rnpExample.
WuonParticle Jul 18, 2025
c1a125d
simplify the randomNumber on the snapshot to reduce cognitive load.
WuonParticle Jul 18, 2025
75c1ef7
minor formatting.
WuonParticle Jul 18, 2025
97bc1e6
separate "Play" tests from the existing test suite. leave shared reco…
WuonParticle Jul 25, 2025
2e90406
rnp spacing changes.
WuonParticle Sep 11, 2025
f114828
fix tests for validateAddress and consolidate validateAddress logic.
WuonParticle Sep 11, 2025
63df89b
remove refresh-templates in favor of setup:with-defaults
WuonParticle Sep 15, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions .cursor/rules/record-and-play-testing-guide.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
---
description: Comprehensive guide for creating tests using the Record and Play (RnP) framework
globs: ["test-record/**/*.record.test.ts", "test-play/**/*.play.test.ts", "test/**/*.test-harness.ts", "test/**/*.api-test-cases.ts"]
alwaysApply: false
---
This guide provides the complete instructions for creating tests using the project's "Record and Play" (RnP) framework. It is intended to be used by LLM agents to autonomously create, run, and maintain tests for new and existing features. Adherence to these rules is not optional; it is critical for the stability and predictability of the testing suite.

This framework applies to any major feature area, including `chains` (e.g., `solana`) and `connectors` (e.g., `raydium`). The `rnpExample` directory is the canonical source for all examples referenced below.

## 1. Philosophy: Why Record and Play?

The RnP framework is designed to create high-fidelity tests that are both robust and easy to maintain. The core philosophy is:
* **Record against reality:** Tests are initially run against live, real-world services to record their actual responses. This captures the true behavior of our dependencies.
* **"Play" in isolation:** Once recorded, tests are run in a "Play" mode that uses the saved recordings (mocks) instead of making live network calls. This makes the tests fast, deterministic, and isolated from external failures.
* **Confidence:** This approach ensures that our application logic is tested against realistic data, which gives us high confidence that it will work correctly in production. It prevents "mock drift," where mocks no longer reflect the reality of the API they are simulating.

## 2. Core Components

The RnP framework consists of four key types of files that work together for a given feature.

### `*.test-harness.ts` - The Engine
* **Location:** `test/{chains|connectors}/{feature-name}/{feature-name}.test-harness.ts`
* **Example:** `test/rnpExample/rnpExample.test-harness.ts`
* **Purpose:** The harness is the central engine of a test suite. It is responsible for initializing the application instance under test and, most importantly, defining the `dependencyContracts`.
* **The `dependencyContracts` Object:** This object is a manifest of all external dependencies. As seen in `rnpExample.test-harness.ts`, it maps a human-readable name (e.g., `dep1_A`) to a dependency contract. This mapping is what allows the framework to intercept calls for recording or mocking.

### `*.api-test-cases.ts` - The Single Source of Truth
* **Location:** `test/{chains|connectors}/{feature-name}/{feature-name}.api-test-cases.ts`
* **Example:** `test/rnpExample/rnpExample.api-test-cases.ts`
* **Purpose:** This file defines the specific API requests that will be used for both recording and testing. By defining test cases in one place (e.g., `export const callABC = new TestCase(...)`), we prevent code duplication and ensure the "Record" and "Play" tests are perfectly aligned.

### `*.record.test.ts` - The "Record" Tests
* **Location:** `test-record/{chains|connectors}/{feature-name}/`
* **Example:** `test-record/rnpExample/rnpExample.record.test.ts`
* **Purpose:** The sole responsibility of this test suite is to generate mock files. It imports a `TestCase` and uses the `createRecordTest(harness)` method to generate one-line `it` blocks that executes the test against live services.
* **Execution:** "Record" tests are slow, make real network calls, and should be run individually using the `pnpm test-record -t "your exact test name"` command.

### `*.play.test.ts` - The "Play" Tests
* **Location:** `test-play/{chains|connectors}/{feature-name}/`
* **Example:** `test-play/rnpExample/rnpExample.play.test.ts`
* **Purpose:** This is the fast, isolated unit test suite. It imports a `TestCase` and uses the `createPlayTest(harness)` method to generate one-line `it` blocks that validate application logic against generated mocks.
* **Execution:** These tests are fast and run as part of the main `pnpm test` suite.

## 3. Directory and Naming Conventions

The framework relies on a strict set of conventions. These are functional requirements, not style suggestions. The `rnpExample` has a simplified API source structure and should not be followed for new development; use the `solana` or `raydium` structure as your template.

### Directory Structure for Chains
* **Source Code:** `src/chains/solana/`
* **Shared Test Artifacts:** `test/chains/solana/` (for harness and API test cases)
* **"Play" Tests:** `test-play/chains/solana/`
* **"Record" Tests:** `test-record/chains/solana/`

### Directory Structure for Connectors
* **Source Code:** `src/connectors/raydium/`
* **Shared Test Artifacts:** `test/connectors/raydium/` (for harness and API test cases)
* **"Play" Tests:** `test-play/connectors/raydium/`
* **"Record" Tests:** `test-record/connectors/raydium/`

### Test Naming
The `describe()` and `it()` block names in the "Record" and "Play" tests **MUST MATCH EXACTLY**. The test case's variable name is used for the `it` block's name to ensure consistency. Compare the `it('callABC', ...)` blocks in `rnpExample.record.test.ts` and `rnpExample.play.test.ts` to see this in practice. This naming convention is how Jest associates API response snapshots with the correct test.

### Command Segregation
* **To run all fast "Play" tests:**
```bash
pnpm test-play
```
* **To run a slow "Record" test and generate mocks:**
```bash
pnpm test-record -t "your exact test name"
```

## 4. The Critical Rule of Dependency Management

To ensure unit tests are fast and never make accidental live network calls, the framework enforces a strict safety policy. Understanding this is not optional.
* When you add even one method from a dependency object (e.g., `Dependency1.A_basicMethod`) to the `dependencyContracts` in `rnpExample.test-harness.ts`, the *entire object* (`dep1`) is now considered "managed."
* **In "Record" Mode:** Managed dependencies behave as expected. The listed methods are spied on, and unlisted methods on the same object (like `Dependency1.unmappedMethod`) call their real implementation.
* **In "Play" Mode:** This is where the safety rule applies. **Every method** on a managed object must be explicitly mocked. If a method (like `Dependency1.unmappedMethod`) is called without a mock, the test will fail. The `callUnmappedMethodMocked` test case in `rnpExample.api-test-cases.ts` is designed specifically to validate this failure.
* **Why?** This strictness forces the agent developer to be fully aware of every interaction with an external service. It makes it impossible for a dependency to add a new network call that would slow down unit tests.
* **Truly Unmanaged Dependencies:** In contrast, `dep2` from `rnpExample` is not mentioned in the `dependencyContracts`. It is "unmanaged," so its methods can be called freely in either mode. The `callUnlistedDep` test case demonstrates this.

## 5. Workflow: Adding a New Endpoint Test

Follow this step-by-step process. In these paths, `{feature-type}` is either `chains` or `connectors`, and `{feature-name}` is the name of your chain or connector (e.g., `solana`, `raydium`).

**Step 1: Define the Test Case**
* Open or create `test/{feature-type}/{feature-name}/{feature-name}.api-test-cases.ts`.
* Create and export a new `TestCase` instance (e.g., `export const callNewFeature = new TestCase(...)`).

**Step 2: Update the Test Harness**
* Open or create `test/{feature-type}/{feature-name}/{feature-name}.test-harness.ts`.
* If your endpoint uses any new dependencies, add them to `dependencyContracts`, as seen in `rnpExample.test-harness.ts`.

**Step 3: Create the "Record" Test**
* Open or create `test-record/{feature-type}/{feature-name}/{feature-name}.record.test.ts`.
* Add a new one-line `it()` block using the `createRecordTest` helper:
```typescript
it('your_test_case_name', yourTestCase.createRecordTest(harness));
```

**Step 4: Run the "Record" Test**
* Execute the "Record" test from your terminal to generate mock and snapshot files.
```bash
pnpm test-record {feature-name}.record.test.ts -t "your test case name"
```

**Step 5: Create the "Play" Test**
* Open or create `test-play/{feature-type}/{feature-name}/{feature-name}.play.test.ts`.
* Add a new one-line `it()` block that **exactly matches** the "Record" test:
```typescript
it('your_test_case_name', yourTestCase.createPlayTest(harness));
```

**Step 6: Run the "Play" Test**
* Execute the main test suite to verify your logic against the generated mocks.
```bash
pnpm test-play {feature-name}.play.test.ts
```
* The test will run, using the mocks you generated. It will pass if the application logic correctly processes the mocked dependency responses to produce the expected final API response.

**Step 7: Run the whole unit test suite**
* Execute the mocked test suite to verify no breaking changes were introduced.
```bash
pnpm test
```
130 changes: 130 additions & 0 deletions CURSOR_VSCODE_SETUP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# VS Code Setup Guide

This guide provides the minimal VS Code configuration needed to work with the Gateway project's dual test suite setup (main tests + RecordAndPlay) and debug the server.

## Jest extension for test discovery and debugging

- **Jest** (`orta.vscode-jest`)

### Settings (`.vscode/settings.json`)

Configure Jest virtual folders for multiple test suites

```json
{
"jest.virtualFolders": [
{
"name": "test-play",
"jestCommandLine": "pnpm test-play",
"runMode": "watch"
},
{
"name": "test-record",
"jestCommandLine": "pnpm test-record",
"runMode": "on-demand"
}
]
}
```

## Debugging the server and test-play

### launch.json (`.vscode/launch.json`)

Launch configuration for debugging the Gateway server and unit tests.

```json
{
"configurations": [
{
"name": "Debug Gateway Server",
"type": "node",
"request": "launch",
"runtimeExecutable": "pnpm",
"runtimeArgs": [
"start"
],
"env": {
"GATEWAY_PASSPHRASE": "${input:password}",
},
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"skipFiles": [
"<node_internals>/**",
"${workspaceFolder}/node_modules/**"
],
"preLaunchTask": "build"
},
{
"name": "vscode-jest-tests.v2.test-play",
"type": "node",
"request": "launch",
"program": "${workspaceFolder}/node_modules/jest/bin/jest.js",
"cwd": "${workspaceFolder}",
"env": {
"GATEWAY_TEST_MODE": "test"
},
"args": [
// Make sure to keep aligned with package.json config
"--verbose",
"--testNamePattern",
"${jest.testNamePattern}",
"--runTestsByPath",
"${jest.testFile}"
],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"skipFiles": [
"<node_internals>/**",
"${workspaceFolder}/node_modules/**"
],
},
{
"name": "vscode-jest-tests.v2.test-record",
"type": "node",
"request": "launch",
"program": "${workspaceFolder}/node_modules/jest/bin/jest.js",
"cwd": "${workspaceFolder}",
"env": {
"GATEWAY_TEST_MODE": "test"
},
"args": [
"--config",
"${workspaceFolder}/test-record/jest.config.js",
"--testNamePattern",
"${jest.testNamePattern}",
"--runTestsByPath",
"${jest.testFile}",
"--runInBand",
"-u"
],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
}
],
"inputs": [
{
"id": "password",
"type": "promptString",
"description": "Specify a password to use for gateway passphrase.",
},
]
}
```

### Tasks (`.vscode/tasks.json`)

Build task for pre-launch compilation

```json
{
"tasks": [
{
"type": "npm",
"script": "build",
"group": "build",
"label": "build"
}
]
}
```
15 changes: 12 additions & 3 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
const { pathsToModuleNameMapper } = require('ts-jest');
const { compilerOptions } = require('./tsconfig.json');
process.env.GATEWAY_TEST_MODE = 'test';

module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
forceExit: true,
detectOpenHandles: false,

coveragePathIgnorePatterns: [
'src/app.ts',
'src/https.ts',
Expand All @@ -19,13 +21,20 @@ module.exports = {
'test/*',
],
modulePathIgnorePatterns: ['<rootDir>/dist/'],
setupFilesAfterEnv: ['<rootDir>/test/jest-setup.js'],
setupFilesAfterEnv: [
'<rootDir>/test/jest-setup.js',
'<rootDir>/test/superjson-setup.ts',
],
testPathIgnorePatterns: [
'/node_modules/',
'/dist/',
'test-helpers',
'<rootDir>/test-scripts/',
],
testMatch: ['<rootDir>/test/**/*.test.ts', '<rootDir>/test/**/*.test.js'],
testMatch: ['<rootDir>/test/**/*.test.ts',
'<rootDir>/test/**/*.test.js',
// NOTE: DOES include play tests, does NOT include record tests
'<rootDir>/test-play/**/*.test.ts',
],
transform: {
'^.+\\.tsx?$': 'ts-jest',
},
Expand Down
12 changes: 7 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,13 @@
"setup:with-defaults": "bash ./gateway-setup.sh --with-defaults",
"start": "START_SERVER=true node dist/index.js",
"copy-files": "copyfiles 'src/templates/namespace/*.json' 'src/templates/*.yml' 'src/templates/chains/**/*.yml' 'src/templates/connectors/*.yml' 'src/templates/tokens/**/*.json' 'src/templates/pools/*.json' 'src/templates/rpc/*.yml' dist",
"test": "GATEWAY_TEST_MODE=dev jest --verbose",
"test": "GATEWAY_TEST_MODE=test jest --verbose",
"test:clear-cache": "jest --clearCache",
"test:debug": "GATEWAY_TEST_MODE=dev jest --watch --runInBand",
"test:unit": "GATEWAY_TEST_MODE=dev jest --runInBand ./test/",
"test:cov": "GATEWAY_TEST_MODE=dev jest --runInBand --coverage ./test/",
"test:scripts": "GATEWAY_TEST_MODE=dev jest --runInBand ./test-scripts/*.test.ts",
"test:debug": "GATEWAY_TEST_MODE=test jest --watch --runInBand",
"test:cov": "GATEWAY_TEST_MODE=test jest --runInBand --coverage ./test/",
"test-record": "GATEWAY_TEST_MODE=test jest --config=test-record/jest.config.js --runInBand -u",
"test-play": "GATEWAY_TEST_MODE=test jest --config=test-play/jest.config.js --runInBand",
"cli": "node dist/index.js",
"typecheck": "tsc --noEmit",
"generate:openapi": "curl http://localhost:15888/docs/json -o openapi.json && echo 'OpenAPI spec saved to openapi.json'",
"rebuild-bigint": "cd node_modules/bigint-buffer && pnpm run rebuild",
Expand Down Expand Up @@ -93,6 +94,7 @@
"pino-pretty": "^11.3.0",
"pnpm": "^10.10.0",
"snake-case": "^4.0.0",
"superjson": "^1.12.2",
"triple-beam": "^1.4.1",
"tslib": "^2.8.1",
"uuid": "^8.3.2",
Expand Down
32 changes: 25 additions & 7 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,13 @@ import { asciiLogo } from './index';
// When false, runs server in HTTPS mode (secure, default for production)
// Use --dev flag to enable HTTP mode, e.g.: pnpm start --dev
// Tests automatically run in dev mode via GATEWAY_TEST_MODE=dev
const devMode = process.argv.includes('--dev') || process.env.GATEWAY_TEST_MODE === 'dev';
const testMode =
process.argv.includes('--test') || process.env.GATEWAY_TEST_MODE === 'test';

const devMode =
process.argv.includes('--dev') ||
process.env.GATEWAY_TEST_MODE === 'dev' ||
testMode;

// Promisify exec for async/await usage
const execPromise = promisify(exec);
Expand Down Expand Up @@ -120,7 +126,7 @@ const swaggerOptions = {
let docsServer: FastifyInstance | null = null;

// Create gateway app configuration function
const configureGatewayServer = () => {
export const configureGatewayServer = () => {
const server = Fastify({
logger: ConfigManagerV2.getInstance().get('server.fastifyLogs')
? {
Expand Down Expand Up @@ -202,6 +208,8 @@ const configureGatewayServer = () => {

// Register routes on both servers
const registerRoutes = async (app: FastifyInstance) => {
app.register(require('@fastify/sensible'));

// Register system routes
app.register(configRoutes, { prefix: '/config' });

Expand Down Expand Up @@ -284,11 +292,21 @@ const configureGatewayServer = () => {
params: request.params,
});

reply.status(500).send({
statusCode: 500,
error: 'Internal Server Error',
message: 'An unexpected error occurred',
});
if (testMode) {
// When in test mode, we want to see the full error stack always
reply.status(500).send({
statusCode: 500,
error: 'Internal Server Error',
stack: error.stack,
message: error.message,
});
} else {
reply.status(500).send({
statusCode: 500,
error: 'Internal Server Error',
message: 'An unexpected error occurred',
});
}
});

// Health check route (outside registerRoutes, only on main server)
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ if (process.env.START_SERVER === 'true') {
console.error('Failed to start server:', error);
process.exit(1);
});
} else {
} else if (process.env.GATEWAY_TEST_MODE !== 'test') {
console.log(asciiLogo);
console.log('Use "pnpm start" to start the Gateway server');
}
Loading