UNPKG

@debugg-ai/debugg-ai-mcp

Version:

Zero-Config, Fully AI-Managed End-to-End Testing for all code gen platforms.

116 lines (115 loc) 3.78 kB
/** * Fault injection + trace collection for tunnel lifecycle debugging (bead 42g). * * This is a TEST/DEV harness. Activation requires BOTH: * - NODE_ENV !== 'production' * - DEBUGG_TUNNEL_FAULT_MODE env var explicitly set * * Modes (comma-separated, parseable by parseFaultMode): * fail-connect-N:<count> — fail the first <count> ngrok.connect() attempts * empty-url-N:<count> — return empty URL from first <count> connect() attempts * delay-connect:<ms> — sleep <ms> before each connect() call * * Examples: * DEBUGG_TUNNEL_FAULT_MODE=fail-connect-N:2 * DEBUGG_TUNNEL_FAULT_MODE=delay-connect:2000,fail-connect-N:1 */ export function parseFaultMode(raw) { if (!raw) return null; const mode = {}; for (const token of raw.split(',').map((s) => s.trim()).filter(Boolean)) { const m = token.match(/^(fail-connect-N|empty-url-N|delay-connect):(\d+)$/); if (!m) continue; const [, name, valStr] = m; const val = parseInt(valStr, 10); if (name === 'fail-connect-N') mode.failConnectN = val; else if (name === 'empty-url-N') mode.emptyUrlN = val; else if (name === 'delay-connect') mode.delayConnectMs = val; } return Object.keys(mode).length > 0 ? mode : null; } export function getFaultModeFromEnv() { if (process.env.NODE_ENV === 'production') return null; return parseFaultMode(process.env.DEBUGG_TUNNEL_FAULT_MODE); } /** * Per-call, mutable fault-injection state. Tracks remaining fault counts so * a 'fail first N' mode applies to the first N attempts within one call, not * forever. */ export class FaultInjector { failConnectRemaining; emptyUrlRemaining; delayMs; constructor(mode) { this.failConnectRemaining = mode?.failConnectN ?? 0; this.emptyUrlRemaining = mode?.emptyUrlN ?? 0; this.delayMs = mode?.delayConnectMs ?? 0; } /** Returns true if this attempt should be forced to fail. Consumes the counter. */ shouldFailConnect() { if (this.failConnectRemaining > 0) { this.failConnectRemaining -= 1; return true; } return false; } /** Returns true if this attempt should return an empty URL. Consumes the counter. */ shouldReturnEmptyUrl() { if (this.emptyUrlRemaining > 0) { this.emptyUrlRemaining -= 1; return true; } return false; } delayMsForAttempt() { return this.delayMs; } /** For diagnostic logging — what's left after in-flight consumption. */ snapshot() { return { failConnectRemaining: this.failConnectRemaining, emptyUrlRemaining: this.emptyUrlRemaining, delayMs: this.delayMs, }; } } export class TunnelTrace { startTime; events = []; constructor(startTime = Date.now()) { this.startTime = startTime; } emit(event, context) { const now = Date.now(); this.events.push({ timestamp: now, elapsedMs: now - this.startTime, event, context, }); } toJSON() { const last = this.events[this.events.length - 1]; return { startTime: this.startTime, durationMs: last ? last.elapsedMs : 0, events: this.events, }; } /** Human-readable one-line-per-event dump, newest last. */ format() { return this.events .map((e) => { const ctx = e.context ? ' ' + JSON.stringify(e.context) : ''; return `+${e.elapsedMs.toString().padStart(6)}ms ${e.event}${ctx}`; }) .join('\n'); } }