feat: Add functional health checks for test prerequisites (#65)

Replaces basic "service is up" checks with functional verification that tests can actually use the services.

## Changes

### New Health Checks
- **RPC Proxy**: Verifies eth_call works and deployed contracts are accessible
- **GraphQL**: Confirms Ponder has indexed data with non-zero stats
- **Web App**: Validates endpoint accessibility

### Improvements
- Clear error messages explain what failed and how to fix it
- Checks verify actual functionality, not just HTTP 200 responses
- Fails fast before tests run with cryptic errors

### Files
- `tests/setup/health-checks.ts` - Core health check functions
- `tests/setup/stack.ts` - Integration with waitForStackReady()
- `tests/HEALTH_CHECKS.md` - Documentation and troubleshooting guide

## Error Message Example

Before:
```
RPC health check failed with status 404
```

After:
```
 Stack health check failed

Failed services:
  • RPC Proxy: RPC proxy returned HTTP 404
    Expected 200, got 404. Check if Anvil is running and RPC_URL is correct.
  • GraphQL Indexer: GraphQL has no indexed data yet
    Ponder is running but has not indexed contract events.

Troubleshooting:
  1. Check stack logs: tail tests/.stack.log
  2. Verify services are running: ./scripts/dev.sh status
  3. Restart stack: ./scripts/dev.sh restart --full
```

## Benefits
-  Tests fail fast with clear error messages
-  Catches configuration issues before tests run
-  Verifies services are actually usable, not just running

resolves #61

Co-authored-by: johba <johba@harb.eth>
Reviewed-on: https://codeberg.org/johba/harb/pulls/65
This commit is contained in:
johba 2025-10-05 20:03:07 +02:00
parent 1645865c5a
commit b1f40374cd
3 changed files with 472 additions and 42 deletions

View file

@ -3,6 +3,11 @@ import { readFile } from 'node:fs/promises';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { promisify } from 'node:util';
import {
runAllHealthChecks,
formatHealthCheckError,
type HealthCheckResult,
} from './health-checks.js';
const exec = promisify(execCallback);
@ -33,44 +38,19 @@ async function run(command: string): Promise<void> {
await exec(command, { cwd: repoRoot, shell: true });
}
async function checkRpcReady(rpcUrl: string): Promise<void> {
const response = await fetch(rpcUrl, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_chainId', params: [] }),
});
/**
* Run functional health checks that verify services are actually usable, not just running
*/
async function checkStackFunctional(
rpcUrl: string,
webAppUrl: string,
graphqlUrl: string
): Promise<void> {
const results = await runAllHealthChecks({ rpcUrl, webAppUrl, graphqlUrl });
if (!response.ok) {
throw new Error(`RPC health check failed with status ${response.status}`);
}
const payload = await response.json();
if (!payload?.result) {
throw new Error('RPC health check returned no result');
}
}
async function checkWebAppReady(webAppUrl: string): Promise<void> {
const url = webAppUrl.endsWith('/') ? `${webAppUrl}app/` : `${webAppUrl}/app/`;
const response = await fetch(url, {
method: 'GET',
redirect: 'manual',
});
if (!response.ok && response.status !== 308) {
throw new Error(`Web app health check failed with status ${response.status}`);
}
}
async function checkGraphqlReady(graphqlUrl: string): Promise<void> {
const response = await fetch(graphqlUrl, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ query: '{ __typename }' }),
});
if (!response.ok) {
throw new Error(`GraphQL health check failed with status ${response.status}`);
const failures = results.filter(r => !r.success);
if (failures.length > 0) {
throw new Error(formatHealthCheckError(results));
}
}
@ -103,11 +83,8 @@ export async function waitForStackReady(options: {
while (Date.now() - start < timeoutMs) {
try {
await Promise.all([
checkRpcReady(rpcUrl),
checkWebAppReady(webAppUrl),
checkGraphqlReady(graphqlUrl),
]);
await checkStackFunctional(rpcUrl, webAppUrl, graphqlUrl);
console.log('✅ All stack health checks passed');
return;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);