From e0145fd24a6776f3815fa8d4a099e57742db5e4f Mon Sep 17 00:00:00 2001 From: Chase Adams Date: Wed, 30 Sep 2026 21:38:00 -0700 Subject: [PATCH] refactor: validate runtime fault network choices --- scripts/runtime-faults.test.ts | 6 +++++- scripts/runtime-faults.ts | 3 ++- scripts/runtime-load.ts | 11 +++++++---- scripts/runtime-network.ts | 19 +++++++++++-------- 4 files changed, 25 insertions(+), 14 deletions(-) diff --git a/scripts/runtime-faults.test.ts b/scripts/runtime-faults.test.ts index 0018f6ad..b0eefe8f 100644 --- a/scripts/runtime-faults.test.ts +++ b/scripts/runtime-faults.test.ts @@ -4,7 +4,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { test } from "vitest"; import { applyFault, type FaultRuntime } from "./runtime-faults.ts"; -import { netemOptions } from "./runtime-network.ts"; +import { netemOptions, parseNetworkPath } from "./runtime-network.ts"; function faultRuntime(overrides: Partial): FaultRuntime { return { @@ -77,6 +77,10 @@ test("a partial blackhole restores applied rules and retains cleanup failures", }); test("network impairment inputs reject malformed or out-of-range values", () => { + assert.equal(parseNetworkPath("forwarding"), "forwarding"); + assert.equal(parseNetworkPath("publisher"), "publisher"); + assert.throws(() => parseNetworkPath(undefined), /invalid network path/); + assert.throws(() => parseNetworkPath("other"), /invalid network path/); assert.deepEqual(netemOptions("latency", "20ms", undefined, "7"), ["delay", "10ms", "seed", "7"]); assert.deepEqual(netemOptions("packet-loss", undefined, "0.1", "0"), ["loss", "random", "0.1%"]); assert.throws( diff --git a/scripts/runtime-faults.ts b/scripts/runtime-faults.ts index c44b717a..3b06c6cd 100644 --- a/scripts/runtime-faults.ts +++ b/scripts/runtime-faults.ts @@ -5,6 +5,7 @@ import { impairmentEndpoints, netemOptions, publisherServices, + type NetworkPath, type RuntimeService, } from "./runtime-network.ts"; import { parseJSON } from "./validation.ts"; @@ -44,7 +45,7 @@ export type FaultRuntime = { readonly ingresses: readonly ("ingress-a" | "ingress-b")[]; readonly activePublishers: readonly (typeof publisherServices)[number][]; readonly network: { - readonly path: string | undefined; + readonly path: NetworkPath; readonly rtt: string | undefined; readonly loss: string | undefined; readonly seed: string | undefined; diff --git a/scripts/runtime-load.ts b/scripts/runtime-load.ts index ff750b7d..ce33e0de 100644 --- a/scripts/runtime-load.ts +++ b/scripts/runtime-load.ts @@ -6,7 +6,7 @@ import { join, resolve } from "node:path"; import { setTimeout as sleep } from "node:timers/promises"; import * as z from "zod"; import { applyFault, faultSchema, type FaultEvents, type FaultRuntime } from "./runtime-faults.ts"; -import { publisherServices } from "./runtime-network.ts"; +import { parseNetworkPath, publisherServices } from "./runtime-network.ts"; import { parseJSON } from "./validation.ts"; const containerSchema = z.object({ @@ -36,7 +36,10 @@ const routes = z.coerce .max(10_000) .parse(process.env.PUBLIC_URLS ?? "4"); const haTopology = z.enum(["0", "1"]).parse(process.env.HA_TOPOLOGY ?? "1") === "1"; -const replicatedRelays = process.env.SCENARIO === "control-restart"; +const scenario = z + .union([z.enum(["relay-restart", "control-restart"]), faultSchema]) + .parse(process.env.SCENARIO ?? "relay-restart"); +const replicatedRelays = scenario === "control-restart"; // each publisher component owns striped pairs of public URLs. small smoke runs // intentionally leave some publisher components without a public URL. const activePublishers = publisherServices.filter((_, shard) => shard * 2 < routes); @@ -79,7 +82,7 @@ let faultError: unknown; const faultState = { intentionalRelayExit: false }; let faultStarted = false; let interrupted = false; -const externalFault = faultSchema.safeParse(process.env.SCENARIO).success; +const externalFault = faultSchema.safeParse(scenario).success; for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) process.on(signal, () => { interrupted = true; @@ -208,7 +211,7 @@ try { ingresses, activePublishers, network: { - path: process.env.NETWORK_PATH, + path: parseNetworkPath(process.env.NETWORK_PATH ?? "forwarding"), rtt: process.env.RTT, loss: process.env.LOSS, seed: process.env.SEED, diff --git a/scripts/runtime-network.ts b/scripts/runtime-network.ts index 8e003c4a..47d44279 100644 --- a/scripts/runtime-network.ts +++ b/scripts/runtime-network.ts @@ -8,7 +8,7 @@ const lossSchema = z.enum(["0.1", "1"]); const seedSchema = z.coerce.number().pipe(z.int().min(0).max(0xffffffff)); export type ImpairmentScenario = "latency" | "packet-loss"; -export type NetworkPath = "forwarding" | "publisher"; +export type NetworkPath = z.infer; export const publisherServices = [ "publishers", "publishers-2", @@ -27,18 +27,21 @@ export interface ImpairmentEndpoint { readonly service: RuntimeService; } +export function parseNetworkPath(value: string | undefined): NetworkPath { + const selected = pathSchema.safeParse(value); + if (!selected.success) throw new Error("invalid network path"); + return selected.data; +} + export function impairmentEndpoints( - path: string | undefined, + path: NetworkPath, addresses: Readonly>, ingresses: readonly ("ingress-a" | "ingress-b")[], activePublishers: readonly (typeof publisherServices)[number][], ): [ImpairmentEndpoint, ...ImpairmentEndpoint[]] { - const selected = pathSchema.safeParse(path); - if (!selected.success) throw new Error("invalid network path"); - const sources: readonly RuntimeService[] = - selected.data === "forwarding" ? ingresses : activePublishers; - const port = selected.data === "forwarding" ? "8443" : "443"; - const protocols = selected.data === "forwarding" ? ["6"] : ["6", "17"]; + const sources: readonly RuntimeService[] = path === "forwarding" ? ingresses : activePublishers; + const port = path === "forwarding" ? "8443" : "443"; + const protocols = path === "forwarding" ? ["6"] : ["6", "17"]; const forwards = sources.map((service): ImpairmentEndpoint => ({ service, filters: [] })); const first = forwards[0]; if (!first) throw new Error("network path requires a source");