@mizchi/playwright-faults 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Page-lifecycle fault injection.
3
+ *
4
+ * Distinct from network-side `FaultRule` (request-scoped, applied via
5
+ * Playwright `route()`). These are page-scoped client-side perturbations
6
+ * applied at well-defined stages of a page visit — CPU throttle, storage
7
+ * wipe, Service Worker cache eviction, key/value tampering. The crawler
8
+ * decides when to call into the executor; the executor decides how to
9
+ * realise each `LifecycleAction` against the browser.
10
+ *
11
+ * Pure helpers (compile / match / probability roll / name derivation) live
12
+ * here so the routing logic is unit-testable without a real browser. The
13
+ * Playwright-backed executor lives at the bottom of this file.
14
+ */
15
+ /** Auto-derive a stats label when the user didn't set `fault.name`. */
16
+ export function lifecycleFaultName(fault) {
17
+ if (fault.name)
18
+ return fault.name;
19
+ const a = fault.action;
20
+ switch (a.kind) {
21
+ case "cpu-throttle":
22
+ return `cpu-throttle:${a.rate}x`;
23
+ case "clear-storage":
24
+ return `clear-storage:${a.scopes.join("+")}`;
25
+ case "evict-cache":
26
+ return a.cacheNames && a.cacheNames.length > 0
27
+ ? `evict-cache:${a.cacheNames.join("+")}`
28
+ : "evict-cache";
29
+ case "tamper-storage":
30
+ return `tamper-storage:${a.scope}.${a.key}`;
31
+ }
32
+ }
33
+ function compilePattern(matcher) {
34
+ if (matcher === undefined)
35
+ return null;
36
+ return matcher instanceof RegExp ? matcher : new RegExp(matcher);
37
+ }
38
+ export function compileLifecycleFaults(faults) {
39
+ if (!faults || faults.length === 0)
40
+ return [];
41
+ return faults.map((fault) => ({
42
+ fault,
43
+ pattern: compilePattern(fault.urlPattern),
44
+ name: lifecycleFaultName(fault),
45
+ matched: 0,
46
+ fired: 0,
47
+ errored: 0,
48
+ }));
49
+ }
50
+ /** True when `compiled.pattern` matches `url` (or no pattern was set). */
51
+ export function lifecycleMatchesUrl(compiled, url) {
52
+ return compiled.pattern === null || compiled.pattern.test(url);
53
+ }
54
+ /**
55
+ * Roll the seeded RNG against `probability`. Returns true when the fault
56
+ * should fire. `prob >= 1` (or undefined) always fires; `prob <= 0` never
57
+ * fires; anything in between samples one number from `rng`.
58
+ *
59
+ * RNG consumption is deliberately conditional on `prob < 1` so that adding
60
+ * a probability-1 fault to a config doesn't shift the seed sequence for
61
+ * existing chaos action selection.
62
+ */
63
+ export function shouldFireProbability(prob, rng) {
64
+ if (prob === undefined || prob >= 1)
65
+ return true;
66
+ if (prob <= 0)
67
+ return false;
68
+ return rng.next() < prob;
69
+ }
70
+ /** Pick the compiled faults that target a given lifecycle stage. */
71
+ export function lifecycleFaultsAtStage(compiled, stage) {
72
+ return compiled.filter((c) => c.fault.when === stage);
73
+ }
74
+ export function lifecycleStatsFrom(compiled) {
75
+ return compiled.map((c) => ({
76
+ name: c.name,
77
+ matched: c.matched,
78
+ fired: c.fired,
79
+ errored: c.errored,
80
+ }));
81
+ }
82
+ /** Dispatch a single `LifecycleAction` to the right executor method. */
83
+ export async function executeLifecycleAction(action, executor) {
84
+ switch (action.kind) {
85
+ case "cpu-throttle":
86
+ await executor.cpuThrottle(action.rate);
87
+ return;
88
+ case "clear-storage":
89
+ await executor.clearStorage(action.scopes);
90
+ return;
91
+ case "evict-cache":
92
+ await executor.evictCache(action.cacheNames);
93
+ return;
94
+ case "tamper-storage":
95
+ await executor.tamperStorage(action.scope, action.key, action.value);
96
+ return;
97
+ }
98
+ }
99
+ /**
100
+ * Real executor backed by Playwright. CPU throttle requires a CDP session;
101
+ * we attach lazily and reuse across calls on the same page.
102
+ */
103
+ export class PlaywrightLifecycleExecutor {
104
+ page;
105
+ context;
106
+ cdp = null;
107
+ constructor(page, context) {
108
+ this.page = page;
109
+ this.context = context;
110
+ }
111
+ getCdp() {
112
+ if (this.cdp === null) {
113
+ this.cdp = this.context.newCDPSession(this.page);
114
+ }
115
+ return this.cdp;
116
+ }
117
+ async cpuThrottle(rate) {
118
+ const client = await this.getCdp();
119
+ await client.send("Emulation.setCPUThrottlingRate", { rate });
120
+ }
121
+ async clearStorage(scopes) {
122
+ const inPage = scopes.filter((s) => s !== "cookies");
123
+ if (inPage.length > 0) {
124
+ // page.evaluate runs in the page context; we pass the scope set so the
125
+ // browser side decides which storages to wipe without us injecting JS.
126
+ await this.page.evaluate(async (scopeList) => {
127
+ const s = new Set(scopeList);
128
+ if (s.has("localStorage")) {
129
+ try {
130
+ window.localStorage.clear();
131
+ }
132
+ catch {
133
+ /* SecurityError on opaque origins */
134
+ }
135
+ }
136
+ if (s.has("sessionStorage")) {
137
+ try {
138
+ window.sessionStorage.clear();
139
+ }
140
+ catch {
141
+ /* SecurityError on opaque origins */
142
+ }
143
+ }
144
+ if (s.has("indexedDB") && "indexedDB" in window) {
145
+ // databases() is not in every browser; guard.
146
+ // @ts-ignore - older lib.dom typings omit databases()
147
+ const dbs = (await indexedDB.databases?.()) ?? [];
148
+ await Promise.all(dbs
149
+ .map((d) => d.name)
150
+ .filter((n) => typeof n === "string")
151
+ .map((name) => new Promise((resolve) => {
152
+ const req = indexedDB.deleteDatabase(name);
153
+ req.onsuccess = () => resolve();
154
+ req.onerror = () => resolve();
155
+ req.onblocked = () => resolve();
156
+ })));
157
+ }
158
+ }, inPage);
159
+ }
160
+ if (scopes.includes("cookies")) {
161
+ // Context-level: drops every cookie across every page in the context.
162
+ await this.context.clearCookies();
163
+ }
164
+ }
165
+ async evictCache(cacheNames) {
166
+ await this.page.evaluate(async (names) => {
167
+ if (!("caches" in self))
168
+ return;
169
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
170
+ const c = self.caches;
171
+ const all = await c.keys();
172
+ const target = names && names.length > 0 ? all.filter((k) => names.includes(k)) : all;
173
+ await Promise.all(target.map((k) => c.delete(k)));
174
+ }, cacheNames);
175
+ }
176
+ async tamperStorage(scope, key, value) {
177
+ await this.page.evaluate(({ scope, key, value }) => {
178
+ try {
179
+ (scope === "localStorage" ? window.localStorage : window.sessionStorage).setItem(key, value);
180
+ }
181
+ catch {
182
+ /* SecurityError on opaque origins */
183
+ }
184
+ }, { scope, key, value });
185
+ }
186
+ }
187
+ //# sourceMappingURL=lifecycle-faults.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lifecycle-faults.js","sourceRoot":"","sources":["../src/lifecycle-faults.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAsBH,uEAAuE;AACvE,MAAM,UAAU,kBAAkB,CAAC,KAAqB;IACtD,IAAI,KAAK,CAAC,IAAI;QAAE,OAAO,KAAK,CAAC,IAAI,CAAC;IAClC,MAAM,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC;IACvB,QAAQ,CAAC,CAAC,IAAI,EAAE,CAAC;QACf,KAAK,cAAc;YACjB,OAAO,gBAAgB,CAAC,CAAC,IAAI,GAAG,CAAC;QACnC,KAAK,eAAe;YAClB,OAAO,iBAAiB,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QAC/C,KAAK,aAAa;YAChB,OAAO,CAAC,CAAC,UAAU,IAAI,CAAC,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC;gBAC5C,CAAC,CAAC,eAAe,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE;gBACzC,CAAC,CAAC,aAAa,CAAC;QACpB,KAAK,gBAAgB;YACnB,OAAO,kBAAkB,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,GAAG,EAAE,CAAC;IAChD,CAAC;AACH,CAAC;AAED,SAAS,cAAc,CAAC,OAA+B;IACrD,IAAI,OAAO,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IACvC,OAAO,OAAO,YAAY,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,CAAC;AACnE,CAAC;AAED,MAAM,UAAU,sBAAsB,CACpC,MAAoC;IAEpC,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAC9C,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;QAC5B,KAAK;QACL,OAAO,EAAE,cAAc,CAAC,KAAK,CAAC,UAAU,CAAC;QACzC,IAAI,EAAE,kBAAkB,CAAC,KAAK,CAAC;QAC/B,OAAO,EAAE,CAAC;QACV,KAAK,EAAE,CAAC;QACR,OAAO,EAAE,CAAC;KACX,CAAC,CAAC,CAAC;AACN,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,mBAAmB,CACjC,QAAiD,EACjD,GAAW;IAEX,OAAO,QAAQ,CAAC,OAAO,KAAK,IAAI,IAAI,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACjE,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,qBAAqB,CACnC,IAAwB,EACxB,GAAuB;IAEvB,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IACjD,IAAI,IAAI,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IAC5B,OAAO,GAAG,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC;AAC3B,CAAC;AAED,oEAAoE;AACpE,MAAM,UAAU,sBAAsB,CACpC,QAA2C,EAC3C,KAAqB;IAErB,OAAO,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,KAAK,KAAK,CAAC,CAAC;AACxD,CAAC;AAED,MAAM,UAAU,kBAAkB,CAChC,QAA2C;IAE3C,OAAO,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAC1B,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,OAAO,EAAE,CAAC,CAAC,OAAO;QAClB,KAAK,EAAE,CAAC,CAAC,KAAK;QACd,OAAO,EAAE,CAAC,CAAC,OAAO;KACnB,CAAC,CAAC,CAAC;AACN,CAAC;AAcD,wEAAwE;AACxE,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,MAAuB,EACvB,QAAiC;IAEjC,QAAQ,MAAM,CAAC,IAAI,EAAE,CAAC;QACpB,KAAK,cAAc;YACjB,MAAM,QAAQ,CAAC,WAAW,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;YACxC,OAAO;QACT,KAAK,eAAe;YAClB,MAAM,QAAQ,CAAC,YAAY,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;YAC3C,OAAO;QACT,KAAK,aAAa;YAChB,MAAM,QAAQ,CAAC,UAAU,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;YAC7C,OAAO;QACT,KAAK,gBAAgB;YACnB,MAAM,QAAQ,CAAC,aAAa,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC;YACrE,OAAO;IACX,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,OAAO,2BAA2B;IAInB;IACA;IAJX,GAAG,GAA+B,IAAI,CAAC;IAE/C,YACmB,IAAU,EACV,OAAuB;QADvB,SAAI,GAAJ,IAAI,CAAM;QACV,YAAO,GAAP,OAAO,CAAgB;IACvC,CAAC;IAEI,MAAM;QACZ,IAAI,IAAI,CAAC,GAAG,KAAK,IAAI,EAAE,CAAC;YACtB,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACnD,CAAC;QACD,OAAO,IAAI,CAAC,GAAG,CAAC;IAClB,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,IAAY;QAC5B,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC;QACnC,MAAM,MAAM,CAAC,IAAI,CAAC,gCAAgC,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;IAChE,CAAC;IAED,KAAK,CAAC,YAAY,CAChB,MAAgF;QAEhF,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC;QACrD,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACtB,uEAAuE;YACvE,uEAAuE;YACvE,MAAM,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,SAA4B,EAAE,EAAE;gBAC9D,MAAM,CAAC,GAAG,IAAI,GAAG,CAAC,SAAS,CAAC,CAAC;gBAC7B,IAAI,CAAC,CAAC,GAAG,CAAC,cAAc,CAAC,EAAE,CAAC;oBAC1B,IAAI,CAAC;wBACH,MAAM,CAAC,YAAY,CAAC,KAAK,EAAE,CAAC;oBAC9B,CAAC;oBAAC,MAAM,CAAC;wBACP,qCAAqC;oBACvC,CAAC;gBACH,CAAC;gBACD,IAAI,CAAC,CAAC,GAAG,CAAC,gBAAgB,CAAC,EAAE,CAAC;oBAC5B,IAAI,CAAC;wBACH,MAAM,CAAC,cAAc,CAAC,KAAK,EAAE,CAAC;oBAChC,CAAC;oBAAC,MAAM,CAAC;wBACP,qCAAqC;oBACvC,CAAC;gBACH,CAAC;gBACD,IAAI,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC,IAAI,WAAW,IAAI,MAAM,EAAE,CAAC;oBAChD,8CAA8C;oBAC9C,sDAAsD;oBACtD,MAAM,GAAG,GAA6B,CAAC,MAAM,SAAS,CAAC,SAAS,EAAE,EAAE,CAAC,IAAI,EAAE,CAAC;oBAC5E,MAAM,OAAO,CAAC,GAAG,CACf,GAAG;yBACA,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;yBAClB,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC;yBACjD,GAAG,CACF,CAAC,IAAI,EAAE,EAAE,CACP,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;wBAC5B,MAAM,GAAG,GAAG,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC;wBAC3C,GAAG,CAAC,SAAS,GAAG,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC;wBAChC,GAAG,CAAC,OAAO,GAAG,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC;wBAC9B,GAAG,CAAC,SAAS,GAAG,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC;oBAClC,CAAC,CAAC,CACL,CACJ,CAAC;gBACJ,CAAC;YACH,CAAC,EAAE,MAAM,CAAC,CAAC;QACb,CAAC;QACD,IAAI,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC;YAC/B,sEAAsE;YACtE,MAAM,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE,CAAC;QACpC,CAAC;IACH,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,UAA8B;QAC7C,MAAM,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,KAAoC,EAAE,EAAE;YACtE,IAAI,CAAC,CAAC,QAAQ,IAAI,IAAI,CAAC;gBAAE,OAAO;YAChC,8DAA8D;YAC9D,MAAM,CAAC,GAAI,IAAY,CAAC,MAAsB,CAAC;YAC/C,MAAM,GAAG,GAAG,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;YAC3B,MAAM,MAAM,GAAG,KAAK,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;YACtF,MAAM,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACpD,CAAC,EAAE,UAAU,CAAC,CAAC;IACjB,CAAC;IAED,KAAK,CAAC,aAAa,CACjB,KAAwC,EACxC,GAAW,EACX,KAAa;QAEb,MAAM,IAAI,CAAC,IAAI,CAAC,QAAQ,CACtB,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,EAA4E,EAAE,EAAE;YAClG,IAAI,CAAC;gBACH,CAAC,KAAK,KAAK,cAAc,CAAC,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;YAC/F,CAAC;YAAC,MAAM,CAAC;gBACP,qCAAqC;YACvC,CAAC;QACH,CAAC,EACD,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,CACtB,CAAC;IACJ,CAAC;CACF"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Tiny seeded RNG used by the package's own unit tests so they don't have
3
+ * to depend on chaosbringer. Not part of the public surface — only re-export
4
+ * from `index.ts` if a downstream legitimately needs the same generator
5
+ * function (most won't; they'll bring their own RNG).
6
+ */
7
+ import type { Rng } from "./types.js";
8
+ export interface SeededRng extends Rng {
9
+ readonly seed: number;
10
+ }
11
+ export declare function createRng(seed: number): SeededRng;
12
+ //# sourceMappingURL=random.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"random.d.ts","sourceRoot":"","sources":["../src/random.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AAEtC,MAAM,WAAW,SAAU,SAAQ,GAAG;IACpC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAYjD"}
package/dist/random.js ADDED
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Tiny seeded RNG used by the package's own unit tests so they don't have
3
+ * to depend on chaosbringer. Not part of the public surface — only re-export
4
+ * from `index.ts` if a downstream legitimately needs the same generator
5
+ * function (most won't; they'll bring their own RNG).
6
+ */
7
+ export function createRng(seed) {
8
+ let state = seed >>> 0;
9
+ return {
10
+ seed,
11
+ next() {
12
+ state = (state + 0x6d2b79f5) >>> 0;
13
+ let t = state;
14
+ t = Math.imul(t ^ (t >>> 15), t | 1);
15
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
16
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
17
+ },
18
+ };
19
+ }
20
+ //# sourceMappingURL=random.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"random.js","sourceRoot":"","sources":["../src/random.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAQH,MAAM,UAAU,SAAS,CAAC,IAAY;IACpC,IAAI,KAAK,GAAG,IAAI,KAAK,CAAC,CAAC;IACvB,OAAO;QACL,IAAI;QACJ,IAAI;YACF,KAAK,GAAG,CAAC,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC;YACnC,IAAI,CAAC,GAAG,KAAK,CAAC;YACd,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;YACrC,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,GAAG,EAAE,CAAC,CAAC;YAC1C,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,UAAU,CAAC;QAC/C,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,71 @@
1
+ /**
2
+ * JS-runtime fault injection.
3
+ *
4
+ * Distinct from `FaultRule` (request-scoped, applied via Playwright `route()`)
5
+ * and `LifecycleFault` (one-shot at named stages of a page visit). These are
6
+ * persistent monkey-patches injected into every page via `addInitScript`,
7
+ * subverting in-page JS APIs (fetch / Date / storage / addEventListener) so
8
+ * the app sees client-side failures that no network mock would expose.
9
+ *
10
+ * Examples:
11
+ * - `flaky-fetch`: `window.fetch` rejects with a TypeError before any
12
+ * network round-trip — simulates "Failed to fetch" / DNS down / Service
13
+ * Worker reject. Different from `faults.status(500)`, which still
14
+ * resolves the promise.
15
+ * - `clock-skew`: `Date.now` / `performance.now` are shifted forward by N
16
+ * ms — exposes token-expiry / cache-bust bugs on long sessions.
17
+ *
18
+ * Pure helpers (`buildRuntimeFaultsScript`, `runtimeFaultName`,
19
+ * `compileRuntimeFaults`) generate / serialize the init script and roll
20
+ * probability — unit-testable without a browser. Stats are reported by the
21
+ * in-page script via a known `window.__chaosbringerRuntimeStats` global; the
22
+ * crawler reads it after each page visit.
23
+ */
24
+ import type { Rng, RuntimeFault, RuntimeFaultStats } from "./types.js";
25
+ /** Compiled form: regex pre-compiled, name pre-derived. */
26
+ export interface CompiledRuntimeFault {
27
+ fault: RuntimeFault;
28
+ /** null when `fault.urlPattern` was omitted. */
29
+ pattern: RegExp | null;
30
+ name: string;
31
+ matched: number;
32
+ fired: number;
33
+ }
34
+ /** Auto-derive a stats label when the user didn't set `fault.name`. */
35
+ export declare function runtimeFaultName(fault: RuntimeFault): string;
36
+ export declare function compileRuntimeFaults(faults: RuntimeFault[] | undefined): CompiledRuntimeFault[];
37
+ /** True when `compiled.pattern` matches `url` (or no pattern was set). */
38
+ export declare function runtimeMatchesUrl(compiled: Pick<CompiledRuntimeFault, "pattern">, url: string): boolean;
39
+ /**
40
+ * Decide whether a probabilistic fault fires this time. Mirrors the
41
+ * lifecycle / network helpers so all three layers share a deterministic
42
+ * roll behaviour given the same RNG.
43
+ */
44
+ export declare function shouldFireProbability(probability: number | undefined, rng: Rng): boolean;
45
+ /**
46
+ * Build the init script body. Self-contained IIFE — no closure over the
47
+ * caller's scope, no external imports — because Playwright serializes init
48
+ * scripts as plain text and runs them in a fresh frame on every navigation.
49
+ *
50
+ * `seed` lets each page roll deterministic probabilities. Pass the
51
+ * crawler's seed so a `(seed, runtimeFaults)` pair always produces the same
52
+ * pattern of injections.
53
+ */
54
+ export declare function buildRuntimeFaultsScript(faults: ReadonlyArray<RuntimeFault>, seed: number): string;
55
+ /**
56
+ * Read the in-page stats counter and merge into the compiled-fault counters
57
+ * (`matched` and `fired`). Returns the merged stats; the compiled-fault
58
+ * objects are mutated in place so the next page picks up where this one
59
+ * left off.
60
+ *
61
+ * Stats keys are array indices so two faults with the same name don't
62
+ * collide their counters. Both legacy name-keyed snapshots and the
63
+ * current index-keyed shape are accepted: name-keyed entries are applied
64
+ * to the first compiled fault with that name (the safe, non-collapsing
65
+ * fallback used when reading older traces).
66
+ */
67
+ export declare function mergeRuntimeStats(compiled: CompiledRuntimeFault[], pageStats: Record<string, {
68
+ matched: number;
69
+ fired: number;
70
+ }>): RuntimeFaultStats[];
71
+ //# sourceMappingURL=runtime-faults.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime-faults.d.ts","sourceRoot":"","sources":["../src/runtime-faults.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,YAAY,EAAE,iBAAiB,EAAc,MAAM,YAAY,CAAC;AAEnF,2DAA2D;AAC3D,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,YAAY,CAAC;IACpB,gDAAgD;IAChD,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,uEAAuE;AACvE,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,YAAY,GAAG,MAAM,CAS5D;AAOD,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,YAAY,EAAE,GAAG,SAAS,GACjC,oBAAoB,EAAE,CASxB;AAED,0EAA0E;AAC1E,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,IAAI,CAAC,oBAAoB,EAAE,SAAS,CAAC,EAC/C,GAAG,EAAE,MAAM,GACV,OAAO,CAET;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,EAAE,GAAG,EAAE,GAAG,GAAG,OAAO,CAKxF;AAYD;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,aAAa,CAAC,YAAY,CAAC,EACnC,IAAI,EAAE,MAAM,GACX,MAAM,CA0GR;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,oBAAoB,EAAE,EAChC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,GAC5D,iBAAiB,EAAE,CAuBrB"}
@@ -0,0 +1,231 @@
1
+ /**
2
+ * JS-runtime fault injection.
3
+ *
4
+ * Distinct from `FaultRule` (request-scoped, applied via Playwright `route()`)
5
+ * and `LifecycleFault` (one-shot at named stages of a page visit). These are
6
+ * persistent monkey-patches injected into every page via `addInitScript`,
7
+ * subverting in-page JS APIs (fetch / Date / storage / addEventListener) so
8
+ * the app sees client-side failures that no network mock would expose.
9
+ *
10
+ * Examples:
11
+ * - `flaky-fetch`: `window.fetch` rejects with a TypeError before any
12
+ * network round-trip — simulates "Failed to fetch" / DNS down / Service
13
+ * Worker reject. Different from `faults.status(500)`, which still
14
+ * resolves the promise.
15
+ * - `clock-skew`: `Date.now` / `performance.now` are shifted forward by N
16
+ * ms — exposes token-expiry / cache-bust bugs on long sessions.
17
+ *
18
+ * Pure helpers (`buildRuntimeFaultsScript`, `runtimeFaultName`,
19
+ * `compileRuntimeFaults`) generate / serialize the init script and roll
20
+ * probability — unit-testable without a browser. Stats are reported by the
21
+ * in-page script via a known `window.__chaosbringerRuntimeStats` global; the
22
+ * crawler reads it after each page visit.
23
+ */
24
+ /** Auto-derive a stats label when the user didn't set `fault.name`. */
25
+ export function runtimeFaultName(fault) {
26
+ if (fault.name)
27
+ return fault.name;
28
+ const a = fault.action;
29
+ switch (a.kind) {
30
+ case "flaky-fetch":
31
+ return "flaky-fetch";
32
+ case "clock-skew":
33
+ return `clock-skew:${a.skewMs}ms`;
34
+ }
35
+ }
36
+ function compilePattern(matcher) {
37
+ if (matcher === undefined)
38
+ return null;
39
+ return matcher instanceof RegExp ? matcher : new RegExp(matcher);
40
+ }
41
+ export function compileRuntimeFaults(faults) {
42
+ if (!faults || faults.length === 0)
43
+ return [];
44
+ return faults.map((fault) => ({
45
+ fault,
46
+ pattern: compilePattern(fault.urlPattern),
47
+ name: runtimeFaultName(fault),
48
+ matched: 0,
49
+ fired: 0,
50
+ }));
51
+ }
52
+ /** True when `compiled.pattern` matches `url` (or no pattern was set). */
53
+ export function runtimeMatchesUrl(compiled, url) {
54
+ return compiled.pattern === null || compiled.pattern.test(url);
55
+ }
56
+ /**
57
+ * Decide whether a probabilistic fault fires this time. Mirrors the
58
+ * lifecycle / network helpers so all three layers share a deterministic
59
+ * roll behaviour given the same RNG.
60
+ */
61
+ export function shouldFireProbability(probability, rng) {
62
+ const p = probability ?? 1;
63
+ if (p >= 1)
64
+ return true;
65
+ if (p <= 0)
66
+ return false;
67
+ return rng.next() < p;
68
+ }
69
+ /**
70
+ * Serialize a UrlMatcher into a structure the in-page script can rebuild
71
+ * without `eval`. Strings stay strings; RegExp becomes `{ source, flags }`.
72
+ */
73
+ function serializeMatcher(m) {
74
+ if (m === undefined)
75
+ return null;
76
+ if (m instanceof RegExp)
77
+ return { source: m.source, flags: m.flags };
78
+ return { source: m, flags: "" };
79
+ }
80
+ /**
81
+ * Build the init script body. Self-contained IIFE — no closure over the
82
+ * caller's scope, no external imports — because Playwright serializes init
83
+ * scripts as plain text and runs them in a fresh frame on every navigation.
84
+ *
85
+ * `seed` lets each page roll deterministic probabilities. Pass the
86
+ * crawler's seed so a `(seed, runtimeFaults)` pair always produces the same
87
+ * pattern of injections.
88
+ */
89
+ export function buildRuntimeFaultsScript(faults, seed) {
90
+ // Stats keys are indices, not names — two faults can legitimately share
91
+ // a name (`flaky-fetch` x2 with different urlPatterns) and we mustn't
92
+ // collapse their counters.
93
+ const serialized = faults.map((f, i) => ({
94
+ id: i,
95
+ name: runtimeFaultName(f),
96
+ pattern: serializeMatcher(f.urlPattern),
97
+ probability: typeof f.probability === "number" ? f.probability : 1,
98
+ action: f.action,
99
+ }));
100
+ // Body of the init script. Indented for readability; whitespace is fine
101
+ // because Playwright won't minify it.
102
+ return `(() => {
103
+ if (typeof window === "undefined") return;
104
+ if (window.__chaosbringerRuntimeFaultsInstalled) return;
105
+ window.__chaosbringerRuntimeFaultsInstalled = true;
106
+ window.__chaosbringerRuntimeStats = {};
107
+
108
+ // Park-Miller LCG — small, deterministic, good enough for fault rolls.
109
+ let __rng = ${seed >>> 0} || 1;
110
+ const __nextRoll = () => {
111
+ __rng = ((__rng * 16807) % 2147483647) | 0;
112
+ if (__rng <= 0) __rng += 2147483647;
113
+ return (__rng - 1) / 2147483646;
114
+ };
115
+
116
+ const faults = ${JSON.stringify(serialized)};
117
+ const stats = window.__chaosbringerRuntimeStats;
118
+ for (const f of faults) stats[String(f.id)] = { matched: 0, fired: 0 };
119
+
120
+ const matchUrl = (pattern, url) => {
121
+ if (!pattern) return true;
122
+ try {
123
+ return new RegExp(pattern.source, pattern.flags).test(url);
124
+ } catch {
125
+ return false;
126
+ }
127
+ };
128
+
129
+ const roll = (f) => {
130
+ const slot = stats[String(f.id)];
131
+ slot.matched++;
132
+ if (f.probability >= 1) {
133
+ slot.fired++;
134
+ return true;
135
+ }
136
+ if (f.probability <= 0) return false;
137
+ const fired = __nextRoll() < f.probability;
138
+ if (fired) slot.fired++;
139
+ return fired;
140
+ };
141
+
142
+ // --- flaky-fetch ---
143
+ const fetchFaults = faults.filter((f) => f.action.kind === "flaky-fetch");
144
+ if (fetchFaults.length > 0 && typeof window.fetch === "function") {
145
+ const realFetch = window.fetch.bind(window);
146
+ window.fetch = function chaosFetch(input, init) {
147
+ const url =
148
+ typeof input === "string" ? input :
149
+ input instanceof URL ? input.toString() :
150
+ (input && typeof input.url === "string") ? input.url :
151
+ "";
152
+ for (const f of fetchFaults) {
153
+ if (matchUrl(f.pattern, url) && roll(f)) {
154
+ const msg = f.action.rejectionMessage || "chaosbringer: simulated fetch failure";
155
+ return Promise.reject(new TypeError(msg));
156
+ }
157
+ }
158
+ return realFetch(input, init);
159
+ };
160
+ }
161
+
162
+ // --- clock-skew ---
163
+ const skewFaults = faults.filter((f) => f.action.kind === "clock-skew");
164
+ if (skewFaults.length > 0) {
165
+ // Use a plain accumulator. Multi-day skews (e.g. 30 days ~ 2.6e9 ms)
166
+ // exceed int32 max, so any '| 0' truncation here would flip them negative.
167
+ let totalSkew = 0;
168
+ for (const f of skewFaults) {
169
+ if (matchUrl(f.pattern, location.href) && roll(f)) {
170
+ totalSkew += Number(f.action.skewMs);
171
+ }
172
+ }
173
+ if (totalSkew !== 0) {
174
+ const realDateNow = Date.now.bind(Date);
175
+ Date.now = () => realDateNow() + totalSkew;
176
+ const realPerfNow = performance.now.bind(performance);
177
+ performance.now = () => realPerfNow() + totalSkew;
178
+ // Patch the Date constructor so \`new Date()\` (no args) also skews.
179
+ const RealDate = Date;
180
+ const SkewedDate = function (...args) {
181
+ if (args.length === 0) return new RealDate(realDateNow() + totalSkew);
182
+ // @ts-ignore
183
+ return new RealDate(...args);
184
+ };
185
+ SkewedDate.now = Date.now;
186
+ SkewedDate.UTC = RealDate.UTC;
187
+ SkewedDate.parse = RealDate.parse;
188
+ SkewedDate.prototype = RealDate.prototype;
189
+ // @ts-ignore
190
+ window.Date = SkewedDate;
191
+ }
192
+ }
193
+ })();`;
194
+ }
195
+ /**
196
+ * Read the in-page stats counter and merge into the compiled-fault counters
197
+ * (`matched` and `fired`). Returns the merged stats; the compiled-fault
198
+ * objects are mutated in place so the next page picks up where this one
199
+ * left off.
200
+ *
201
+ * Stats keys are array indices so two faults with the same name don't
202
+ * collide their counters. Both legacy name-keyed snapshots and the
203
+ * current index-keyed shape are accepted: name-keyed entries are applied
204
+ * to the first compiled fault with that name (the safe, non-collapsing
205
+ * fallback used when reading older traces).
206
+ */
207
+ export function mergeRuntimeStats(compiled, pageStats) {
208
+ for (let i = 0; i < compiled.length; i++) {
209
+ const c = compiled[i];
210
+ // Index-keyed (current shape).
211
+ const ps = pageStats[String(i)];
212
+ if (ps) {
213
+ c.matched += ps.matched;
214
+ c.fired += ps.fired;
215
+ continue;
216
+ }
217
+ // Backwards-compat: name-keyed (one slot per distinct name only;
218
+ // applied to the first compiled fault that wears that name).
219
+ const byName = pageStats[c.name];
220
+ if (byName && !compiled.slice(0, i).some((c2) => c2.name === c.name)) {
221
+ c.matched += byName.matched;
222
+ c.fired += byName.fired;
223
+ }
224
+ }
225
+ return compiled.map((c) => ({
226
+ rule: c.name,
227
+ matched: c.matched,
228
+ fired: c.fired,
229
+ }));
230
+ }
231
+ //# sourceMappingURL=runtime-faults.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime-faults.js","sourceRoot":"","sources":["../src/runtime-faults.ts"],"names":[],"mappings":"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"}