From 651457e355c70c5597c91cb9abb2cebade1b648e Mon Sep 17 00:00:00 2001 From: aaseenib Date: Thu, 26 Mar 2026 11:16:00 +0100 Subject: [PATCH] feat: add failure harness template --- README.md | 3 ++ docs/failure-harness.md | 13 +++++ tooling/failure-harness-template/README.md | 13 +++++ .../failure-harness.middleware.ts | 48 +++++++++++++++++++ .../failure-harness.routes.ts | 23 +++++++++ .../failure-harness.state.ts | 22 +++++++++ .../failure-harness.types.ts | 18 +++++++ tooling/failure-harness-template/index.ts | 4 ++ 8 files changed, 144 insertions(+) create mode 100644 docs/failure-harness.md create mode 100644 tooling/failure-harness-template/README.md create mode 100644 tooling/failure-harness-template/failure-harness.middleware.ts create mode 100644 tooling/failure-harness-template/failure-harness.routes.ts create mode 100644 tooling/failure-harness-template/failure-harness.state.ts create mode 100644 tooling/failure-harness-template/failure-harness.types.ts create mode 100644 tooling/failure-harness-template/index.ts diff --git a/README.md b/README.md index c032058..0d277b2 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,6 @@ # Timely Capsule App 🕰️ +## Reference templates + +The repository includes a merge-safe failure harness template in `tooling/failure-harness-template`. diff --git a/docs/failure-harness.md b/docs/failure-harness.md new file mode 100644 index 0000000..77976fb --- /dev/null +++ b/docs/failure-harness.md @@ -0,0 +1,13 @@ +# Failure Harness + +The repository includes a failure harness template for future Express environments. + +## Use cases + +- testing retry behavior +- exercising degraded UI states +- validating request timeout handling + +## Current location + +`tooling/failure-harness-template` diff --git a/tooling/failure-harness-template/README.md b/tooling/failure-harness-template/README.md new file mode 100644 index 0000000..98936b5 --- /dev/null +++ b/tooling/failure-harness-template/README.md @@ -0,0 +1,13 @@ +# Failure Harness Template + +This template provides a controlled way to inject failures into future Express routes. + +## Supported modes + +- latency spike +- random 500 +- timeout +- dropped response +- malformed JSON + +The harness is designed to be opt-in and safe to merge before the API app exists. diff --git a/tooling/failure-harness-template/failure-harness.middleware.ts b/tooling/failure-harness-template/failure-harness.middleware.ts new file mode 100644 index 0000000..f2a8252 --- /dev/null +++ b/tooling/failure-harness-template/failure-harness.middleware.ts @@ -0,0 +1,48 @@ +import type { NextFunction, Request, Response } from "express"; +import type { FailureMode, HarnessState } from "./failure-harness.types"; + +const pickRandom = (items: T[]): T => items[Math.floor(Math.random() * items.length)] as T; + +const applyFailure = ( + mode: FailureMode, + request: Request, + response: Response, + next: NextFunction +) => { + switch (mode) { + case "latency-spike": + setTimeout(next, 1500); + return; + case "random-500": + response.status(500).json({ error: "Injected failure", mode, path: request.path }); + return; + case "timeout": + setTimeout(() => response.end(), 20000); + return; + case "dropped-response": + request.socket.destroy(); + return; + case "malformed-json": + response.type("application/json").send("{\"broken\":"); + return; + } +}; + +export const createFailureHarness = + (state: HarnessState) => + (request: Request, response: Response, next: NextFunction) => { + if (!state.enabled || !state.activeScenario) { + next(); + return; + } + + const rules = state.scenarios[state.activeScenario] ?? []; + const rule = rules.find((candidate) => new RegExp(candidate.pathPattern).test(request.path)); + + if (!rule || Math.random() > rule.probability) { + next(); + return; + } + + applyFailure(pickRandom(rule.modes), request, response, next); + }; diff --git a/tooling/failure-harness-template/failure-harness.routes.ts b/tooling/failure-harness-template/failure-harness.routes.ts new file mode 100644 index 0000000..55f6548 --- /dev/null +++ b/tooling/failure-harness-template/failure-harness.routes.ts @@ -0,0 +1,23 @@ +import { Router } from "express"; +import type { HarnessState } from "./failure-harness.types"; + +export const createFailureHarnessRouter = (state: HarnessState) => { + const router = Router(); + + router.get("/__sandbox/failure-harness/state", (_request, response) => { + response.json(state); + }); + + router.post("/__sandbox/failure-harness/enable", (request, response) => { + state.enabled = Boolean(request.body?.enabled); + response.json({ enabled: state.enabled }); + }); + + router.post("/__sandbox/failure-harness/scenario", (request, response) => { + const scenario = request.body?.scenario; + state.activeScenario = typeof scenario === "string" ? scenario : null; + response.json({ activeScenario: state.activeScenario }); + }); + + return router; +}; diff --git a/tooling/failure-harness-template/failure-harness.state.ts b/tooling/failure-harness-template/failure-harness.state.ts new file mode 100644 index 0000000..9aee6c9 --- /dev/null +++ b/tooling/failure-harness-template/failure-harness.state.ts @@ -0,0 +1,22 @@ +import type { HarnessState } from "./failure-harness.types"; + +export const createHarnessState = (): HarnessState => ({ + enabled: false, + activeScenario: null, + scenarios: { + "flaky-network": [ + { + pathPattern: ".*", + probability: 0.2, + modes: ["latency-spike", "dropped-response"] + } + ], + "partial-outage": [ + { + pathPattern: "^/(?!health).*", + probability: 0.5, + modes: ["random-500", "timeout"] + } + ] + } +}); diff --git a/tooling/failure-harness-template/failure-harness.types.ts b/tooling/failure-harness-template/failure-harness.types.ts new file mode 100644 index 0000000..430223d --- /dev/null +++ b/tooling/failure-harness-template/failure-harness.types.ts @@ -0,0 +1,18 @@ +export type FailureMode = + | "latency-spike" + | "random-500" + | "timeout" + | "dropped-response" + | "malformed-json"; + +export interface HarnessRule { + pathPattern: string; + probability: number; + modes: FailureMode[]; +} + +export interface HarnessState { + enabled: boolean; + activeScenario: string | null; + scenarios: Record; +} diff --git a/tooling/failure-harness-template/index.ts b/tooling/failure-harness-template/index.ts new file mode 100644 index 0000000..1133f14 --- /dev/null +++ b/tooling/failure-harness-template/index.ts @@ -0,0 +1,4 @@ +export * from "./failure-harness.types"; +export * from "./failure-harness.state"; +export * from "./failure-harness.middleware"; +export * from "./failure-harness.routes";