@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
JavaScript
/**
* 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');
}
}