diff --git a/README.md b/README.md index 438f566..4143eb2 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,5 @@ # ourKairos -## Repository scaffolding - -The repository includes merge-safe implementation templates for work that will land incrementally. - `tooling/api-module-template` contains the baseline Express module structure for future API features. - `docs/api-architecture.md` documents the intended module registration pattern. 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";