Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
19 changes: 16 additions & 3 deletions sdks/typescript/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions sdks/typescript/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "test-server-sdk",
"version": "0.2.1",
"version": "0.2.2",
"description": "TypeScript SDK for test-server",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand All @@ -27,7 +27,8 @@
"dependencies": {
"axios": "^1.6.0",
"extract-zip": "^2.0.1",
"tar": "^6.2.0"
"tar": "^6.2.0",
"yaml": "^2.8.0"
},
"devDependencies": {
"@types/node": "^20.11.0",
Expand Down
5 changes: 3 additions & 2 deletions sdks/typescript/sample/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion sdks/typescript/sample/src/specs/sample.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ describe('Sample Test Suite (with test-server)', () => {

beforeAll(async () => {
try {
serverProcess = startTestServer(sampleTestServerOptions);
serverProcess = await startTestServer(sampleTestServerOptions);
console.log(`[SampleSpec] test-server started with PID: ${serverProcess.pid}. Waiting for it to be ready...`);
// TODO(amirh): Replace this with some sort of a readiness check.
await new Promise(resolve => setTimeout(resolve, 500));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
endpoints:
- target_host: www.github.com
- target_host: github.com
target_type: https
target_port: 443
source_type: http
source_port: 18080
health: healthz
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
b4d6e60a9b97e7b98c63df9308728c5c88c0b40c398046772c63447b94608b4d
Server Address: github.com
Port: 443
Protocol: https
********************************************************************************
GET /healthz HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Accept-Language: *
Connection: keep-alive
Sec-Fetch-Mode: cors
User-Agent: node


Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
Status code: 200
Date: Wed, 11 Jun 2025 21:20:28 GMT
Accept-Ranges: bytes
Set-Cookie: _gh_sess=RUXx6I8rXzwAWTFBzCcNLMpA%2BIfAw19leo8%2F6hxTBGEr2XfLP%2BZPLguz%2BpQPQZL%2FWUfw2tJpqLFdQ0ihM1LGU9TJZ2Eel46thCU8OnCKlZMJ2RGVKcQr%2FBQuwO32xXFMbywgBNRfsyNpat0U%2FEQqQRx2Gx9kwLxwliM6o%2BWPU3XHZvnfJRSMjbkdLIT2UDrTPOg9yYE3xUEd9M3q%2BbG5vPsTWY5IJleB%2Brfx9DhiYzrjlDWEjc%2FVuTpn6bWHmELj119LAt3P83gw%2Fc1pT9NNhw%3D%3D--WP%2FOWsLLuU0K7OWX--s%2B8qDjT7cpMciWgIREzfYw%3D%3D; Path=/; HttpOnly; Secure; SameSite=Lax
Set-Cookie: _octo=GH1.1.499478081.1749676828; Path=/; Domain=github.com; Expires=Thu, 11 Jun 2026 21:20:28 GMT; Secure; SameSite=Lax
Set-Cookie: logged_in=no; Path=/; Domain=github.com; Expires=Thu, 11 Jun 2026 21:20:28 GMT; HttpOnly; Secure; SameSite=Lax
Content-Length: 58
Cache-Control: no-cache
X-Frame-Options: DENY
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Content-Type: text/html
X-Github-Request-Id: 751F:290523:51C25B9:54C9428:6849F31C

<html><body><h1>200 OK</h1>
Service ready.
</body></html>
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
b4d6e60a9b97e7b98c63df9308728c5c88c0b40c398046772c63447b94608b4d
Server Address: www.github.com
Server Address: github.com
Port: 443
Protocol: https
********************************************************************************
Expand Down

Large diffs are not rendered by default.

54 changes: 53 additions & 1 deletion sdks/typescript/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import { spawn, ChildProcess } from 'child_process';
import * as path from 'path';
import * as fs from 'fs';
import { parse } from 'yaml';

const PROJECT_NAME = 'test-server';

Expand Down Expand Up @@ -67,7 +68,7 @@ export interface TestServerOptions {
* @param options Configuration options for starting the server.
* @returns The spawned ChildProcess instance.
*/
export function startTestServer(options: TestServerOptions): ChildProcess {
export async function startTestServer(options: TestServerOptions): Promise<ChildProcess> {
const { configPath, recordingDir, mode: optionsMode, env, onStdOut, onStdErr, onExit, onError } = options;
const binaryPath = getBinaryPath();

Expand Down Expand Up @@ -136,6 +137,7 @@ export function startTestServer(options: TestServerOptions): ChildProcess {
});

console.log(`[test-server-sdk] test-server process (PID: ${serverProcess.pid}) started.`);
await awaitHealthyTestServer(options);
return serverProcess;
}

Expand Down Expand Up @@ -191,3 +193,53 @@ export function stopTestServer(serverProcess: ChildProcess): Promise<void> {
}, 5000); // 5 seconds timeout
});
}

/**
* Waits for the test-server to become healthy by checking the configured health endpoints.
* It reads the config file and performs health checks on all endpoints that have a 'health' path defined.
* It uses a retry mechanism with exponential backoff for health checks.
*
* @param options Configuration options containing the configPath.
*/
async function awaitHealthyTestServer(options: TestServerOptions): Promise<void> {
const configRaw = fs.readFileSync(options.configPath, { encoding: 'utf8' });
const config = parse(configRaw)
for (const endpoint of config['endpoints']) {
if (!("health" in endpoint)) {
continue;
}
const url = `${endpoint['source_type']}://localhost:${endpoint['source_port']}/${endpoint['health']}`
await healthCheck(url);
}
return;
}

/**
* Performs a health check on a given URL with a retry mechanism.
* It retries fetching the URL up to MAX_RETRIES times with an exponential backoff delay.
*
* @param url The URL to check for health.
* @returns A Promise that resolves if the health check is successful.
* @throws An error if the health check fails after all retries.
*/
async function healthCheck(url: string): Promise<void> {
const MAX_RETRIES = 10;
const BASE_DELAY_MS = 100;

for (let i = 0; i < MAX_RETRIES; i++) {
try {
const response = await fetch(url);
if (response.ok) {
return;
}
} catch (error) {
console.warn(`[test-server-sdk] Health check attempt ${i + 1} failed for ${url}. Error: ${error}`);
}

const delay = BASE_DELAY_MS * Math.pow(2, i);
console.log(`[test-server-sdk] Retrying health check for ${url} in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
}

throw new Error(`[test-server-sdk] Health check failed for ${url} after ${MAX_RETRIES} retries.`);
}