@nodetool-ai/reliability-harness 0.8.1

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.
Files changed (167) hide show
  1. package/README.md +34 -0
  2. package/dist/.nodetool-build-stamp +1 -0
  3. package/dist/cli.d.ts +73 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +201 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/compare.d.ts +98 -0
  8. package/dist/compare.d.ts.map +1 -0
  9. package/dist/compare.js +267 -0
  10. package/dist/compare.js.map +1 -0
  11. package/dist/core/diff.d.ts +39 -0
  12. package/dist/core/diff.d.ts.map +1 -0
  13. package/dist/core/diff.js +114 -0
  14. package/dist/core/diff.js.map +1 -0
  15. package/dist/core/golden.d.ts +50 -0
  16. package/dist/core/golden.d.ts.map +1 -0
  17. package/dist/core/golden.js +150 -0
  18. package/dist/core/golden.js.map +1 -0
  19. package/dist/core/invariants/index.d.ts +32 -0
  20. package/dist/core/invariants/index.d.ts.map +1 -0
  21. package/dist/core/invariants/index.js +44 -0
  22. package/dist/core/invariants/index.js.map +1 -0
  23. package/dist/core/invariants/leaks.d.ts +22 -0
  24. package/dist/core/invariants/leaks.d.ts.map +1 -0
  25. package/dist/core/invariants/leaks.js +55 -0
  26. package/dist/core/invariants/leaks.js.map +1 -0
  27. package/dist/core/invariants/lifecycle.d.ts +19 -0
  28. package/dist/core/invariants/lifecycle.d.ts.map +1 -0
  29. package/dist/core/invariants/lifecycle.js +95 -0
  30. package/dist/core/invariants/lifecycle.js.map +1 -0
  31. package/dist/core/invariants/protocol-validity.d.ts +4 -0
  32. package/dist/core/invariants/protocol-validity.d.ts.map +1 -0
  33. package/dist/core/invariants/protocol-validity.js +131 -0
  34. package/dist/core/invariants/protocol-validity.js.map +1 -0
  35. package/dist/core/invariants/state-machine.d.ts +29 -0
  36. package/dist/core/invariants/state-machine.d.ts.map +1 -0
  37. package/dist/core/invariants/state-machine.js +79 -0
  38. package/dist/core/invariants/state-machine.js.map +1 -0
  39. package/dist/core/invariants/status-sets.d.ts +27 -0
  40. package/dist/core/invariants/status-sets.d.ts.map +1 -0
  41. package/dist/core/invariants/status-sets.js +51 -0
  42. package/dist/core/invariants/status-sets.js.map +1 -0
  43. package/dist/core/invariants/terminal-uniqueness.d.ts +23 -0
  44. package/dist/core/invariants/terminal-uniqueness.d.ts.map +1 -0
  45. package/dist/core/invariants/terminal-uniqueness.js +99 -0
  46. package/dist/core/invariants/terminal-uniqueness.js.map +1 -0
  47. package/dist/core/invariants/types.d.ts +25 -0
  48. package/dist/core/invariants/types.d.ts.map +1 -0
  49. package/dist/core/invariants/types.js +2 -0
  50. package/dist/core/invariants/types.js.map +1 -0
  51. package/dist/core/journey.d.ts +121 -0
  52. package/dist/core/journey.d.ts.map +1 -0
  53. package/dist/core/journey.js +166 -0
  54. package/dist/core/journey.js.map +1 -0
  55. package/dist/core/normalize.d.ts +86 -0
  56. package/dist/core/normalize.d.ts.map +1 -0
  57. package/dist/core/normalize.js +182 -0
  58. package/dist/core/normalize.js.map +1 -0
  59. package/dist/core/record.d.ts +160 -0
  60. package/dist/core/record.d.ts.map +1 -0
  61. package/dist/core/record.js +74 -0
  62. package/dist/core/record.js.map +1 -0
  63. package/dist/core/stable-json.d.ts +13 -0
  64. package/dist/core/stable-json.d.ts.map +1 -0
  65. package/dist/core/stable-json.js +28 -0
  66. package/dist/core/stable-json.js.map +1 -0
  67. package/dist/drivers/anchors.d.ts +29 -0
  68. package/dist/drivers/anchors.d.ts.map +1 -0
  69. package/dist/drivers/anchors.js +48 -0
  70. package/dist/drivers/anchors.js.map +1 -0
  71. package/dist/drivers/app-headless.d.ts +39 -0
  72. package/dist/drivers/app-headless.d.ts.map +1 -0
  73. package/dist/drivers/app-headless.js +123 -0
  74. package/dist/drivers/app-headless.js.map +1 -0
  75. package/dist/drivers/browser.d.ts +34 -0
  76. package/dist/drivers/browser.d.ts.map +1 -0
  77. package/dist/drivers/browser.js +176 -0
  78. package/dist/drivers/browser.js.map +1 -0
  79. package/dist/drivers/cli.d.ts +13 -0
  80. package/dist/drivers/cli.d.ts.map +1 -0
  81. package/dist/drivers/cli.js +82 -0
  82. package/dist/drivers/cli.js.map +1 -0
  83. package/dist/drivers/fixture-nodes.d.ts +124 -0
  84. package/dist/drivers/fixture-nodes.d.ts.map +1 -0
  85. package/dist/drivers/fixture-nodes.js +189 -0
  86. package/dist/drivers/fixture-nodes.js.map +1 -0
  87. package/dist/drivers/index.d.ts +18 -0
  88. package/dist/drivers/index.d.ts.map +1 -0
  89. package/dist/drivers/index.js +12 -0
  90. package/dist/drivers/index.js.map +1 -0
  91. package/dist/drivers/kernel.d.ts +8 -0
  92. package/dist/drivers/kernel.d.ts.map +1 -0
  93. package/dist/drivers/kernel.js +247 -0
  94. package/dist/drivers/kernel.js.map +1 -0
  95. package/dist/drivers/packaged.d.ts +28 -0
  96. package/dist/drivers/packaged.d.ts.map +1 -0
  97. package/dist/drivers/packaged.js +303 -0
  98. package/dist/drivers/packaged.js.map +1 -0
  99. package/dist/drivers/python-bridge.d.ts +22 -0
  100. package/dist/drivers/python-bridge.d.ts.map +1 -0
  101. package/dist/drivers/python-bridge.js +48 -0
  102. package/dist/drivers/python-bridge.js.map +1 -0
  103. package/dist/drivers/registry.d.ts +11 -0
  104. package/dist/drivers/registry.d.ts.map +1 -0
  105. package/dist/drivers/registry.js +18 -0
  106. package/dist/drivers/registry.js.map +1 -0
  107. package/dist/drivers/relay-fields.d.ts +70 -0
  108. package/dist/drivers/relay-fields.d.ts.map +1 -0
  109. package/dist/drivers/relay-fields.js +104 -0
  110. package/dist/drivers/relay-fields.js.map +1 -0
  111. package/dist/drivers/repo-root.d.ts +9 -0
  112. package/dist/drivers/repo-root.d.ts.map +1 -0
  113. package/dist/drivers/repo-root.js +39 -0
  114. package/dist/drivers/repo-root.js.map +1 -0
  115. package/dist/drivers/spawn-cli.d.ts +8 -0
  116. package/dist/drivers/spawn-cli.d.ts.map +1 -0
  117. package/dist/drivers/spawn-cli.js +41 -0
  118. package/dist/drivers/spawn-cli.js.map +1 -0
  119. package/dist/drivers/types.d.ts +18 -0
  120. package/dist/drivers/types.d.ts.map +1 -0
  121. package/dist/drivers/types.js +2 -0
  122. package/dist/drivers/types.js.map +1 -0
  123. package/dist/drivers/ws-server.d.ts +8 -0
  124. package/dist/drivers/ws-server.d.ts.map +1 -0
  125. package/dist/drivers/ws-server.js +382 -0
  126. package/dist/drivers/ws-server.js.map +1 -0
  127. package/dist/faults/bridge-faults.d.ts +7 -0
  128. package/dist/faults/bridge-faults.d.ts.map +1 -0
  129. package/dist/faults/bridge-faults.js +90 -0
  130. package/dist/faults/bridge-faults.js.map +1 -0
  131. package/dist/faults/host-faults.d.ts +40 -0
  132. package/dist/faults/host-faults.d.ts.map +1 -0
  133. package/dist/faults/host-faults.js +70 -0
  134. package/dist/faults/host-faults.js.map +1 -0
  135. package/dist/faults/index.d.ts +21 -0
  136. package/dist/faults/index.d.ts.map +1 -0
  137. package/dist/faults/index.js +29 -0
  138. package/dist/faults/index.js.map +1 -0
  139. package/dist/faults/net-proxy.d.ts +75 -0
  140. package/dist/faults/net-proxy.d.ts.map +1 -0
  141. package/dist/faults/net-proxy.js +305 -0
  142. package/dist/faults/net-proxy.js.map +1 -0
  143. package/dist/faults/provider-faults.d.ts +12 -0
  144. package/dist/faults/provider-faults.d.ts.map +1 -0
  145. package/dist/faults/provider-faults.js +110 -0
  146. package/dist/faults/provider-faults.js.map +1 -0
  147. package/dist/faults/registry.d.ts +17 -0
  148. package/dist/faults/registry.d.ts.map +1 -0
  149. package/dist/faults/registry.js +15 -0
  150. package/dist/faults/registry.js.map +1 -0
  151. package/dist/faults/types.d.ts +42 -0
  152. package/dist/faults/types.d.ts.map +1 -0
  153. package/dist/faults/types.js +2 -0
  154. package/dist/faults/types.js.map +1 -0
  155. package/dist/faults/ws-faults.d.ts +6 -0
  156. package/dist/faults/ws-faults.d.ts.map +1 -0
  157. package/dist/faults/ws-faults.js +48 -0
  158. package/dist/faults/ws-faults.js.map +1 -0
  159. package/dist/faults/ws-proxy.d.ts +36 -0
  160. package/dist/faults/ws-proxy.d.ts.map +1 -0
  161. package/dist/faults/ws-proxy.js +40 -0
  162. package/dist/faults/ws-proxy.js.map +1 -0
  163. package/dist/index.d.ts +12 -0
  164. package/dist/index.d.ts.map +1 -0
  165. package/dist/index.js +12 -0
  166. package/dist/index.js.map +1 -0
  167. package/package.json +75 -0
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Bridge-seam fault modules (docs/RELIABILITY_ARCHITECTURE.md §9; task D3).
3
+ *
4
+ * Each of the five `bridge-*` fault names in `cli.ts`'s `KNOWN_FAULT_TYPES`
5
+ * drives E2's faithful stdio fake worker
6
+ * (`packages/runtime/tests/fixtures/fake-python-stdio-worker.ts`) with the
7
+ * `FAKE_WORKER_*` env var it already understands — the exact same knobs
8
+ * `packages/runtime/tests/python-stdio-bridge.test.ts` uses directly against
9
+ * `PythonStdioBridge`. `configure()` sets the env vars ambient (the fake
10
+ * interpreter shim spawns with `env: process.env`, see
11
+ * `python-stdio-bridge.ts`'s `_spawnCandidate`), and the teardown restores
12
+ * whatever was there before — same "process-global side effect, always
13
+ * undone" contract `provider-faults.ts` uses for the provider registry.
14
+ *
15
+ * The driver-side wiring that actually spawns the fake worker for a graph
16
+ * that needs one lives in `drivers/python-bridge.ts` (shared by the kernel
17
+ * and ws-server drivers) — these fault modules never touch a driver or a
18
+ * bridge instance directly, only the env vars the next spawn will read.
19
+ */
20
+ import { registerFaultModule } from "./registry.js";
21
+ /** `cli.ts`'s `KNOWN_FAULT_TYPES` `bridge-*` entries, mapped to the
22
+ * `FAKE_WORKER_*` env vars that reproduce each fault against E2's fake
23
+ * (see that fixture's own doc comment for the full knob list). */
24
+ const FAULT_ENV_BY_NAME = {
25
+ // Worker exits without responding once it receives an `execute` request —
26
+ // `PythonStdioBridge.execute()` rejects the pending request when the
27
+ // process exits (`python-stdio-bridge.test.ts`: "rejects pending requests
28
+ // when the worker exits mid-request").
29
+ "bridge-exit-mid-request": { FAKE_WORKER_EXIT_ON_TYPE: "execute" },
30
+ // Worker replies to `execute` with an absurd length prefix instead of a
31
+ // real frame — `FrameDecoder` fails the whole connection
32
+ // (`FrameSizeError`), same path a real desynced/corrupted stream hits.
33
+ "bridge-framing-violation": { FAKE_WORKER_BAD_LENGTH_ON_TYPE: "execute" },
34
+ // Worker never emits "NODETOOL_STDIO_READY" — `connect()`'s startup timer
35
+ // (`FAKE_BRIDGE_STARTUP_TIMEOUT_MS` in `drivers/python-bridge.ts`) fires
36
+ // instead of a normal ready signal.
37
+ "bridge-never-ready": { FAKE_WORKER_MODE: "never-ready" },
38
+ // Worker's stdin read end is closed immediately (before `discover` is even
39
+ // sent, given the ready-signal head start), so the bridge's very first
40
+ // write hits a genuinely broken pipe — the same EPIPE path
41
+ // `python-stdio-bridge.test.ts`'s "fails connect() with EPIPE" test drives.
42
+ "bridge-epipe": {
43
+ FAKE_WORKER_CLOSE_STDIN_AFTER_MS: "0",
44
+ FAKE_WORKER_READY_DELAY_MS: "400"
45
+ },
46
+ // Worker reports a `protocol_version` below `MIN_BRIDGE_PROTOCOL_VERSION`
47
+ // (1, `@nodetool-ai/protocol/bridge-protocol`) — the bridge's hard-floor
48
+ // check in `python-bridge-base.ts`'s discover handler rejects the connect.
49
+ "bridge-version-mismatch": { FAKE_WORKER_PROTOCOL_VERSION: "0" }
50
+ };
51
+ function applyEnvOverrides(env) {
52
+ const previous = {};
53
+ for (const key of Object.keys(env)) {
54
+ previous[key] = process.env[key];
55
+ process.env[key] = env[key];
56
+ }
57
+ return () => {
58
+ for (const key of Object.keys(previous)) {
59
+ const value = previous[key];
60
+ if (value === undefined)
61
+ delete process.env[key];
62
+ else
63
+ process.env[key] = value;
64
+ }
65
+ };
66
+ }
67
+ function configureBridgeFault(name, _ctx) {
68
+ const env = FAULT_ENV_BY_NAME[name];
69
+ if (!env) {
70
+ throw new Error(`bridge-faults.ts: no env mapping for fault "${name}"`);
71
+ }
72
+ return applyEnvOverrides(env);
73
+ }
74
+ function makeBridgeFaultModule(name) {
75
+ return {
76
+ name,
77
+ seam: "bridge",
78
+ configure: (ctx) => configureBridgeFault(name, ctx)
79
+ };
80
+ }
81
+ export const BRIDGE_FAULT_MODULES = Object.keys(FAULT_ENV_BY_NAME).map(makeBridgeFaultModule);
82
+ /** Registers every bridge-seam fault module. Called once from
83
+ * `faults/index.ts`'s module-load side effect (task D1's "call once, own
84
+ * module state" contract) — safe to call again, last-write-wins. */
85
+ export function registerBridgeFaultModules() {
86
+ for (const module of BRIDGE_FAULT_MODULES) {
87
+ registerFaultModule(module);
88
+ }
89
+ }
90
+ //# sourceMappingURL=bridge-faults.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bridge-faults.js","sourceRoot":"","sources":["../../src/faults/bridge-faults.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAGpD;;kEAEkE;AAClE,MAAM,iBAAiB,GAA+D;IACpF,0EAA0E;IAC1E,qEAAqE;IACrE,0EAA0E;IAC1E,uCAAuC;IACvC,yBAAyB,EAAE,EAAE,wBAAwB,EAAE,SAAS,EAAE;IAClE,wEAAwE;IACxE,yDAAyD;IACzD,uEAAuE;IACvE,0BAA0B,EAAE,EAAE,8BAA8B,EAAE,SAAS,EAAE;IACzE,0EAA0E;IAC1E,yEAAyE;IACzE,oCAAoC;IACpC,oBAAoB,EAAE,EAAE,gBAAgB,EAAE,aAAa,EAAE;IACzD,2EAA2E;IAC3E,uEAAuE;IACvE,2DAA2D;IAC3D,4EAA4E;IAC5E,cAAc,EAAE;QACd,gCAAgC,EAAE,GAAG;QACrC,0BAA0B,EAAE,KAAK;KAClC;IACD,0EAA0E;IAC1E,yEAAyE;IACzE,2EAA2E;IAC3E,yBAAyB,EAAE,EAAE,4BAA4B,EAAE,GAAG,EAAE;CACjE,CAAC;AAEF,SAAS,iBAAiB,CAAC,GAAqC;IAC9D,MAAM,QAAQ,GAAuC,EAAE,CAAC;IACxD,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QACnC,QAAQ,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACjC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC;IAC9B,CAAC;IACD,OAAO,GAAG,EAAE;QACV,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;YACxC,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC;YAC5B,IAAI,KAAK,KAAK,SAAS;gBAAE,OAAO,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;;gBAC5C,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;QAChC,CAAC;IACH,CAAC,CAAC;AACJ,CAAC;AAED,SAAS,oBAAoB,CAAC,IAAY,EAAE,IAAkB;IAC5D,MAAM,GAAG,GAAG,iBAAiB,CAAC,IAAI,CAAC,CAAC;IACpC,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,MAAM,IAAI,KAAK,CAAC,+CAA+C,IAAI,GAAG,CAAC,CAAC;IAC1E,CAAC;IACD,OAAO,iBAAiB,CAAC,GAAG,CAAC,CAAC;AAChC,CAAC;AAED,SAAS,qBAAqB,CAAC,IAAY;IACzC,OAAO;QACL,IAAI;QACJ,IAAI,EAAE,QAAQ;QACd,SAAS,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,oBAAoB,CAAC,IAAI,EAAE,GAAG,CAAC;KACpD,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,MAAM,oBAAoB,GAA2B,MAAM,CAAC,IAAI,CACrE,iBAAiB,CAClB,CAAC,GAAG,CAAC,qBAAqB,CAAC,CAAC;AAE7B;;oEAEoE;AACpE,MAAM,UAAU,0BAA0B;IACxC,KAAK,MAAM,MAAM,IAAI,oBAAoB,EAAE,CAAC;QAC1C,mBAAmB,CAAC,MAAM,CAAC,CAAC;IAC9B,CAAC;AACH,CAAC"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Host-seam fault modules (docs/RELIABILITY_ARCHITECTURE.md §9; task D3).
3
+ *
4
+ * Of the three §9 "process/host" faults, only `host-disk-full` is a
5
+ * `FaultModule` here:
6
+ *
7
+ * - `host-disk-full`: an injectable `StorageAdapter` whose writes fail with
8
+ * ENOSPC. Wired into the **kernel driver only** (`drivers/kernel.ts`
9
+ * consults {@link getInjectedHostStorage} when building the run's
10
+ * `ProcessingContext`) — the ws-server driver's asset storage comes from
11
+ * `websocket-client-session.ts`'s own module-level `getTempAdapter()`
12
+ * singleton (`lib/storage.ts`), which task A5 explicitly reserves for a
13
+ * later `ExecutionSession` migration ("do not refactor
14
+ * `websocket-client-session.ts`... pin behavior with tests around it").
15
+ * Journeys exercising this fault must scope it to `"surface": "kernel"`.
16
+ * - `host-db-locked` has no `FaultModule`: the swallowed failure it pins
17
+ * (`websocket-client-session.ts` ~2868) is job-table persistence, which
18
+ * neither driver's hermetic runs touch (both run with `persistence:
19
+ * "session"`/no persistence hook) — see
20
+ * `packages/websocket/tests/job-persistence-failure.test.ts` for the
21
+ * dedicated pin instead. `--faults host-db-locked` correctly reports
22
+ * "unknown fault" from this harness until a journey exists that actually
23
+ * exercises job persistence.
24
+ * - `host-sigkill-restart` has no `FaultModule`: killing and restarting the
25
+ * server process is a harness-level *test* (spawns/kills a real process),
26
+ * not a per-run "configure/teardown around one driver call" fault — see
27
+ * `tests/journeys/host-sigkill-restart.test.ts`.
28
+ */
29
+ import { type StorageAdapter } from "@nodetool-ai/runtime";
30
+ import type { FaultModule } from "./types.js";
31
+ /** Consulted by the kernel driver when building a run's `ProcessingContext` —
32
+ * non-null only while `host-disk-full` is configured for the current
33
+ * `compareJourney` call. */
34
+ export declare function getInjectedHostStorage(): StorageAdapter | null;
35
+ export declare const HOST_FAULT_MODULES: readonly FaultModule[];
36
+ /** Registers every host-seam fault module this harness implements. Called
37
+ * once from `faults/index.ts`'s module-load side effect — safe to call
38
+ * again, last-write-wins. */
39
+ export declare function registerHostFaultModules(): void;
40
+ //# sourceMappingURL=host-faults.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"host-faults.d.ts","sourceRoot":"","sources":["../../src/faults/host-faults.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAU3D,OAAO,KAAK,EAAgB,WAAW,EAAiB,MAAM,YAAY,CAAC;AAsD3E;;4BAE4B;AAC5B,wBAAgB,sBAAsB,IAAI,cAAc,GAAG,IAAI,CAE9D;AAUD,eAAO,MAAM,kBAAkB,EAAE,SAAS,WAAW,EAMpD,CAAC;AAEF;;6BAE6B;AAC7B,wBAAgB,wBAAwB,IAAI,IAAI,CAI/C"}
@@ -0,0 +1,70 @@
1
+ import { InMemoryStorageAdapter } from "@nodetool-ai/storage";
2
+ import { registerFaultModule } from "./registry.js";
3
+ function enospcError(key) {
4
+ const err = new Error(`ENOSPC: no space left on device, write '${key}'`);
5
+ err.code = "ENOSPC";
6
+ return err;
7
+ }
8
+ /** Wraps a real adapter, failing every WRITE op with an ENOSPC-shaped error —
9
+ * reads still work (a full disk doesn't un-write what's already on it),
10
+ * matching what an actual write failure looks like mid-run. */
11
+ class EnospcStorageAdapter {
12
+ inner;
13
+ constructor(inner) {
14
+ this.inner = inner;
15
+ }
16
+ store(key) {
17
+ throw enospcError(key);
18
+ }
19
+ retrieve(uri) {
20
+ return this.inner.retrieve(uri);
21
+ }
22
+ exists(uri) {
23
+ return this.inner.exists(uri);
24
+ }
25
+ uriForKey(key) {
26
+ return this.inner.uriForKey(key);
27
+ }
28
+ list(prefix, opts) {
29
+ return this.inner.list(prefix, opts);
30
+ }
31
+ delete(uri) {
32
+ throw enospcError(uri);
33
+ }
34
+ stat(uri) {
35
+ return this.inner.stat(uri);
36
+ }
37
+ localPath(uri) {
38
+ return this.inner.localPath(uri);
39
+ }
40
+ }
41
+ let injectedStorage = null;
42
+ /** Consulted by the kernel driver when building a run's `ProcessingContext` —
43
+ * non-null only while `host-disk-full` is configured for the current
44
+ * `compareJourney` call. */
45
+ export function getInjectedHostStorage() {
46
+ return injectedStorage;
47
+ }
48
+ function configureDiskFullFault(_ctx) {
49
+ const previous = injectedStorage;
50
+ injectedStorage = new EnospcStorageAdapter(new InMemoryStorageAdapter());
51
+ return () => {
52
+ injectedStorage = previous;
53
+ };
54
+ }
55
+ export const HOST_FAULT_MODULES = [
56
+ {
57
+ name: "host-disk-full",
58
+ seam: "host",
59
+ configure: configureDiskFullFault
60
+ }
61
+ ];
62
+ /** Registers every host-seam fault module this harness implements. Called
63
+ * once from `faults/index.ts`'s module-load side effect — safe to call
64
+ * again, last-write-wins. */
65
+ export function registerHostFaultModules() {
66
+ for (const module of HOST_FAULT_MODULES) {
67
+ registerFaultModule(module);
68
+ }
69
+ }
70
+ //# sourceMappingURL=host-faults.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"host-faults.js","sourceRoot":"","sources":["../../src/faults/host-faults.ts"],"names":[],"mappings":"AA6BA,OAAO,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAUpD,SAAS,WAAW,CAAC,GAAW;IAC9B,MAAM,GAAG,GAAG,IAAI,KAAK,CACnB,2CAA2C,GAAG,GAAG,CACzB,CAAC;IAC3B,GAAG,CAAC,IAAI,GAAG,QAAQ,CAAC;IACpB,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;+DAE+D;AAC/D,MAAM,oBAAoB;IACK,KAAK;IAAlC,YAA6B,KAAqB;qBAArB,KAAK;IAAmB,CAAC;IAEtD,KAAK,CAAC,GAAW;QACf,MAAM,WAAW,CAAC,GAAG,CAAC,CAAC;IACzB,CAAC;IAED,QAAQ,CAAC,GAAW;QAClB,OAAO,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;IAClC,CAAC;IAED,MAAM,CAAC,GAAW;QAChB,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAChC,CAAC;IAED,SAAS,CAAC,GAAW;QACnB,OAAO,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;IACnC,CAAC;IAED,IAAI,CACF,MAAc,EACd,IAA6B;QAE7B,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IACvC,CAAC;IAED,MAAM,CAAC,GAAW;QAChB,MAAM,WAAW,CAAC,GAAG,CAAC,CAAC;IACzB,CAAC;IAED,IAAI,CAAC,GAAW;QACd,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC9B,CAAC;IAED,SAAS,CAAC,GAAW;QACnB,OAAO,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;IACnC,CAAC;CACF;AAED,IAAI,eAAe,GAA0B,IAAI,CAAC;AAElD;;4BAE4B;AAC5B,MAAM,UAAU,sBAAsB;IACpC,OAAO,eAAe,CAAC;AACzB,CAAC;AAED,SAAS,sBAAsB,CAAC,IAAkB;IAChD,MAAM,QAAQ,GAAG,eAAe,CAAC;IACjC,eAAe,GAAG,IAAI,oBAAoB,CAAC,IAAI,sBAAsB,EAAE,CAAC,CAAC;IACzE,OAAO,GAAG,EAAE;QACV,eAAe,GAAG,QAAQ,CAAC;IAC7B,CAAC,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,MAAM,kBAAkB,GAA2B;IACxD;QACE,IAAI,EAAE,gBAAgB;QACtB,IAAI,EAAE,MAAM;QACZ,SAAS,EAAE,sBAAsB;KAClC;CACF,CAAC;AAEF;;6BAE6B;AAC7B,MAAM,UAAU,wBAAwB;IACtC,KAAK,MAAM,MAAM,IAAI,kBAAkB,EAAE,CAAC;QACxC,mBAAmB,CAAC,MAAM,CAAC,CAAC;IAC9B,CAAC;AACH,CAAC"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Fault layer barrel (task D1). Importing this module registers every
3
+ * built-in fault module as a side effect — the same "import to register"
4
+ * shape `@nodetool-ai/base-nodes`' `registerBaseNodes` and this harness's own
5
+ * `registerFixtureNodes` use — so `compare.ts`/`cli.ts` only need to import
6
+ * `faults/index.js` once to have every known fault name resolvable.
7
+ *
8
+ * D2 (WS proxy) and D3 (bridge/host) add a sibling module file exporting
9
+ * their own `FaultModule[]` plus a `register*FaultModules()` function, call
10
+ * it from this barrel, and are done — `types.ts`/`registry.ts`/`compare.ts`
11
+ * need no changes.
12
+ */
13
+ export * from "./types.js";
14
+ export * from "./registry.js";
15
+ export * from "./provider-faults.js";
16
+ export * from "./net-proxy.js";
17
+ export * from "./ws-proxy.js";
18
+ export * from "./ws-faults.js";
19
+ export * from "./bridge-faults.js";
20
+ export * from "./host-faults.js";
21
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/faults/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,sBAAsB,CAAC;AACrC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Fault layer barrel (task D1). Importing this module registers every
3
+ * built-in fault module as a side effect — the same "import to register"
4
+ * shape `@nodetool-ai/base-nodes`' `registerBaseNodes` and this harness's own
5
+ * `registerFixtureNodes` use — so `compare.ts`/`cli.ts` only need to import
6
+ * `faults/index.js` once to have every known fault name resolvable.
7
+ *
8
+ * D2 (WS proxy) and D3 (bridge/host) add a sibling module file exporting
9
+ * their own `FaultModule[]` plus a `register*FaultModules()` function, call
10
+ * it from this barrel, and are done — `types.ts`/`registry.ts`/`compare.ts`
11
+ * need no changes.
12
+ */
13
+ export * from "./types.js";
14
+ export * from "./registry.js";
15
+ export * from "./provider-faults.js";
16
+ export * from "./net-proxy.js";
17
+ export * from "./ws-proxy.js";
18
+ export * from "./ws-faults.js";
19
+ export * from "./bridge-faults.js";
20
+ export * from "./host-faults.js";
21
+ import { registerProviderFaultModules } from "./provider-faults.js";
22
+ import { registerWsFaultModules } from "./ws-faults.js";
23
+ import { registerBridgeFaultModules } from "./bridge-faults.js";
24
+ import { registerHostFaultModules } from "./host-faults.js";
25
+ registerProviderFaultModules();
26
+ registerWsFaultModules();
27
+ registerBridgeFaultModules();
28
+ registerHostFaultModules();
29
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/faults/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,sBAAsB,CAAC;AACrC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AAEjC,OAAO,EAAE,4BAA4B,EAAE,MAAM,sBAAsB,CAAC;AACpE,OAAO,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,EAAE,0BAA0B,EAAE,MAAM,oBAAoB,CAAC;AAChE,OAAO,EAAE,wBAAwB,EAAE,MAAM,kBAAkB,CAAC;AAE5D,4BAA4B,EAAE,CAAC;AAC/B,sBAAsB,EAAE,CAAC;AACzB,0BAA0B,EAAE,CAAC;AAC7B,wBAAwB,EAAE,CAAC"}
@@ -0,0 +1,75 @@
1
+ /** The six `ws-*` fault kinds §9 names, mapped 1:1 onto `cli.ts`'s
2
+ * `KNOWN_FAULT_TYPES` `ws-*` entries (`ws-faults.ts` does that mapping). */
3
+ export type WsProxyFaultKind = "drop-no-fin" | "delay" | "reorder" | "fragment" | "stall-reads" | "abrupt-close";
4
+ export interface WsProxyFaultConfig {
5
+ kind: WsProxyFaultKind;
6
+ /**
7
+ * Frame-count anchor: the fault arms on the first frame *after* this many
8
+ * frames have already been forwarded in the fault's relevant direction
9
+ * (server->client for every kind except `abrupt-close`, which counts
10
+ * client->server frames — see the class doc). Default 0: arm immediately,
11
+ * i.e. on the very first frame.
12
+ */
13
+ afterFrames?: number;
14
+ /** `delay` only: fixed added latency per chunk, ms. Default 200. */
15
+ delayMs?: number;
16
+ /** `fragment` only: TCP-write chunk size in bytes. Default 4. */
17
+ chunkBytes?: number;
18
+ }
19
+ /**
20
+ * Incrementally counts complete WebSocket frames in a byte stream that may
21
+ * arrive split across many `data` events. Parses only the frame header
22
+ * (RFC 6455 §5.2 — 2-10 header bytes plus an optional 4-byte mask key) to
23
+ * find each frame's total length; payload bytes are never inspected. Good
24
+ * enough for frame-boundary anchoring — not a full WS parser (continuation
25
+ * frames are counted as their own frame, which is fine: this harness's
26
+ * fixture messages are always single-frame sends).
27
+ */
28
+ export declare class WsFrameCounter {
29
+ private buf;
30
+ /** Number of complete frames seen so far. */
31
+ count: number;
32
+ feed(chunk: Buffer): void;
33
+ /** Returns the total byte length of the frame starting at `buf[0]`, or
34
+ * `null` if `buf` doesn't yet hold a complete header. */
35
+ static tryFrameLength(buf: Buffer): number | null;
36
+ }
37
+ /**
38
+ * One TCP proxy instance in front of one `(targetHost, targetPort)`. Every
39
+ * accepted client connection gets its own pair of pipes (client->server,
40
+ * server->client); `fault` (settable any time via {@link setFault}) governs
41
+ * both pipes of every connection accepted while it's set — matching how a
42
+ * real network fault (a flaky link, a saturated NAT table) affects whatever
43
+ * is talking over it, not just connections opened after the fault started.
44
+ */
45
+ export declare class WsFaultProxy {
46
+ private readonly targetHost;
47
+ private readonly targetPort;
48
+ private fault;
49
+ private server;
50
+ private readonly connections;
51
+ private port;
52
+ constructor(targetHost: string, targetPort: number, fault?: WsProxyFaultConfig | null);
53
+ setFault(fault: WsProxyFaultConfig | null): void;
54
+ listen(): Promise<number>;
55
+ getPort(): number;
56
+ close(): Promise<void>;
57
+ /**
58
+ * Forces every currently-open connection's pair dead — used by the
59
+ * client-reconnect-mid-run journey to sever a client mid-run outside the
60
+ * declarative fault matrix. `"reset"` destroys both sockets (an ordinary
61
+ * TCP RST/FIN teardown); `"half-open"` stops all forwarding and pauses
62
+ * both sockets' reads without closing either — the same net-level black
63
+ * hole `drop-no-fin` produces, just triggered on demand instead of after
64
+ * a frame count.
65
+ */
66
+ killConnections(mode: "reset" | "half-open"): void;
67
+ private handleConnection;
68
+ /** Builds the `data`-event handler for one direction of one connection.
69
+ * Per-pipe state (the frame counter, the one-shot flags below) lives in
70
+ * this closure, not on the proxy or the connection entry, so concurrent
71
+ * connections and the two directions of the same connection never share
72
+ * counters. */
73
+ private makePipe;
74
+ }
75
+ //# sourceMappingURL=net-proxy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"net-proxy.d.ts","sourceRoot":"","sources":["../../src/faults/net-proxy.ts"],"names":[],"mappings":"AAqBA;4EAC4E;AAC5E,MAAM,MAAM,gBAAgB,GACxB,aAAa,GACb,OAAO,GACP,SAAS,GACT,UAAU,GACV,aAAa,GACb,cAAc,CAAC;AAEnB,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,gBAAgB,CAAC;IACvB;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,oEAAoE;IACpE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iEAAiE;IACjE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAOD;;;;;;;;GAQG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,GAAG,CAA2B;IACtC,6CAA6C;IAC7C,KAAK,SAAK;IAEV,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAQxB;IAED;6DACyD;IACzD,MAAM,CAAC,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAiBhD;CACF;AAmCD;;;;;;;GAOG;AACH,qBAAa,YAAY;IAMrB,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,KAAK;IAPf,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAgC;IAC5D,OAAO,CAAC,IAAI,CAAK;IAEjB,YACmB,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,EAC3B,KAAK,GAAE,kBAAkB,GAAG,IAAW,EAC7C;IAEJ,QAAQ,CAAC,KAAK,EAAE,kBAAkB,GAAG,IAAI,GAAG,IAAI,CAE/C;IAEK,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC,CAS9B;IAED,OAAO,IAAI,MAAM,CAEhB;IAEK,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAW3B;IAED;;;;;;;;OAQG;IACH,eAAe,CAAC,IAAI,EAAE,OAAO,GAAG,WAAW,GAAG,IAAI,CAUjD;IAED,OAAO,CAAC,gBAAgB;IAwBxB;;;;mBAIe;IACf,OAAO,CAAC,QAAQ;CAuHjB"}
@@ -0,0 +1,305 @@
1
+ /**
2
+ * Net-level TCP fault proxy (docs/RELIABILITY_ARCHITECTURE.md §9 "WS
3
+ * transport" row; task D2). Sits in front of a real TCP server (the
4
+ * ws-server driver's hermetic `e2e-server`) and forwards bytes verbatim in
5
+ * both directions until a configured {@link WsProxyFaultConfig} tells one
6
+ * direction's pipe to misbehave.
7
+ *
8
+ * This is a raw byte-level proxy, not a WebSocket-aware one — it never
9
+ * terminates the WS handshake or decodes frame payloads. The one thing it
10
+ * does parse is the RFC 6455 frame *header* (via {@link WsFrameCounter}), just
11
+ * enough to find frame boundaries in the byte stream so a fault can be
12
+ * anchored on "after N frames" rather than an arbitrary byte count or a
13
+ * wall-clock delay (§9: "event-anchored, not time-anchored", mirroring
14
+ * `drivers/anchors.ts`'s message-type anchors one layer down, at the wire
15
+ * instead of the decoded message).
16
+ *
17
+ * `ws-proxy.ts` is the seam-facing half (process-wide "is a fault active"
18
+ * state the `WsServerDriver` consults); this file is the mechanism.
19
+ */
20
+ import { connect, createServer } from "node:net";
21
+ /**
22
+ * Incrementally counts complete WebSocket frames in a byte stream that may
23
+ * arrive split across many `data` events. Parses only the frame header
24
+ * (RFC 6455 §5.2 — 2-10 header bytes plus an optional 4-byte mask key) to
25
+ * find each frame's total length; payload bytes are never inspected. Good
26
+ * enough for frame-boundary anchoring — not a full WS parser (continuation
27
+ * frames are counted as their own frame, which is fine: this harness's
28
+ * fixture messages are always single-frame sends).
29
+ */
30
+ export class WsFrameCounter {
31
+ buf = Buffer.alloc(0);
32
+ /** Number of complete frames seen so far. */
33
+ count = 0;
34
+ feed(chunk) {
35
+ this.buf = this.buf.length > 0 ? Buffer.concat([this.buf, chunk]) : chunk;
36
+ for (;;) {
37
+ const frameLength = WsFrameCounter.tryFrameLength(this.buf);
38
+ if (frameLength === null || this.buf.length < frameLength)
39
+ return;
40
+ this.count += 1;
41
+ this.buf = this.buf.subarray(frameLength);
42
+ }
43
+ }
44
+ /** Returns the total byte length of the frame starting at `buf[0]`, or
45
+ * `null` if `buf` doesn't yet hold a complete header. */
46
+ static tryFrameLength(buf) {
47
+ if (buf.length < 2)
48
+ return null;
49
+ const secondByte = buf[1];
50
+ const masked = (secondByte & 0x80) !== 0;
51
+ let payloadLength = secondByte & 0x7f;
52
+ let offset = 2;
53
+ if (payloadLength === 126) {
54
+ if (buf.length < 4)
55
+ return null;
56
+ payloadLength = buf.readUInt16BE(2);
57
+ offset = 4;
58
+ }
59
+ else if (payloadLength === 127) {
60
+ if (buf.length < 10)
61
+ return null;
62
+ payloadLength = Number(buf.readBigUInt64BE(2));
63
+ offset = 10;
64
+ }
65
+ if (masked)
66
+ offset += 4;
67
+ return offset + payloadLength;
68
+ }
69
+ }
70
+ /**
71
+ * Writes `chunk` to `socket` in `chunkBytes`-sized pieces. Synchronous and
72
+ * loop-based (not `setImmediate`-deferred) on purpose: this runs inside a
73
+ * `data` event handler, and a deferred, multi-tick version can still be
74
+ * mid-flight when the *next* `data` event fires on the same socket pair —
75
+ * two concurrently-running fragmenters racing to `write()` the same
76
+ * destination socket interleave their pieces and corrupt the byte stream.
77
+ * A single synchronous loop can't race anything: it either fully finishes
78
+ * before the next `data` event is even dispatched, or the destination socket
79
+ * is already destroyed (checked per piece) and the rest is silently dropped.
80
+ */
81
+ function writeFragmented(socket, chunk, chunkBytes) {
82
+ const step = Math.max(1, chunkBytes);
83
+ for (let offset = 0; offset < chunk.length; offset += step) {
84
+ if (socket.destroyed)
85
+ return;
86
+ socket.write(chunk.subarray(offset, Math.min(offset + step, chunk.length)));
87
+ }
88
+ }
89
+ /** Which direction a given fault kind's frame-count anchor applies to.
90
+ * `abrupt-close` models "close hard right after the run_job command is
91
+ * forwarded" (§9) — every journey's first `client_to_server` frame is the
92
+ * `run_job` command envelope (`ws-server.ts`'s driver always sends `run`
93
+ * first), so counting client->server frames with the default `afterFrames:
94
+ * 0` closes right after that command reaches the server. Every other kind
95
+ * targets server->client frames — the direction a client's parser actually
96
+ * has to tolerate misbehaving. */
97
+ function relevantDirection(kind) {
98
+ return kind === "abrupt-close" ? "client-to-server" : "server-to-client";
99
+ }
100
+ /**
101
+ * One TCP proxy instance in front of one `(targetHost, targetPort)`. Every
102
+ * accepted client connection gets its own pair of pipes (client->server,
103
+ * server->client); `fault` (settable any time via {@link setFault}) governs
104
+ * both pipes of every connection accepted while it's set — matching how a
105
+ * real network fault (a flaky link, a saturated NAT table) affects whatever
106
+ * is talking over it, not just connections opened after the fault started.
107
+ */
108
+ export class WsFaultProxy {
109
+ targetHost;
110
+ targetPort;
111
+ fault;
112
+ server = null;
113
+ connections = new Set();
114
+ port = 0;
115
+ constructor(targetHost, targetPort, fault = null) {
116
+ this.targetHost = targetHost;
117
+ this.targetPort = targetPort;
118
+ this.fault = fault;
119
+ }
120
+ setFault(fault) {
121
+ this.fault = fault;
122
+ }
123
+ async listen() {
124
+ this.server = createServer((clientSocket) => this.handleConnection(clientSocket));
125
+ await new Promise((resolve, reject) => {
126
+ this.server.once("error", reject);
127
+ this.server.listen(0, "127.0.0.1", () => resolve());
128
+ });
129
+ const address = this.server.address();
130
+ this.port = typeof address === "object" && address !== null ? address.port : 0;
131
+ return this.port;
132
+ }
133
+ getPort() {
134
+ return this.port;
135
+ }
136
+ async close() {
137
+ for (const conn of this.connections) {
138
+ conn.clientSocket.destroy();
139
+ conn.serverSocket.destroy();
140
+ }
141
+ this.connections.clear();
142
+ const server = this.server;
143
+ this.server = null;
144
+ if (server) {
145
+ await new Promise((resolve) => server.close(() => resolve()));
146
+ }
147
+ }
148
+ /**
149
+ * Forces every currently-open connection's pair dead — used by the
150
+ * client-reconnect-mid-run journey to sever a client mid-run outside the
151
+ * declarative fault matrix. `"reset"` destroys both sockets (an ordinary
152
+ * TCP RST/FIN teardown); `"half-open"` stops all forwarding and pauses
153
+ * both sockets' reads without closing either — the same net-level black
154
+ * hole `drop-no-fin` produces, just triggered on demand instead of after
155
+ * a frame count.
156
+ */
157
+ killConnections(mode) {
158
+ for (const conn of this.connections) {
159
+ if (mode === "reset") {
160
+ conn.clientSocket.destroy();
161
+ conn.serverSocket.destroy();
162
+ }
163
+ else {
164
+ conn.clientSocket.pause();
165
+ conn.serverSocket.pause();
166
+ }
167
+ }
168
+ }
169
+ handleConnection(clientSocket) {
170
+ const serverSocket = connect(this.targetPort, this.targetHost);
171
+ const entry = { clientSocket, serverSocket };
172
+ this.connections.add(entry);
173
+ clientSocket.setNoDelay(true);
174
+ serverSocket.setNoDelay(true);
175
+ let cleaned = false;
176
+ const cleanup = () => {
177
+ if (cleaned)
178
+ return;
179
+ cleaned = true;
180
+ this.connections.delete(entry);
181
+ clientSocket.destroy();
182
+ serverSocket.destroy();
183
+ };
184
+ clientSocket.on("error", cleanup);
185
+ serverSocket.on("error", cleanup);
186
+ clientSocket.on("close", cleanup);
187
+ serverSocket.on("close", cleanup);
188
+ clientSocket.on("data", this.makePipe(clientSocket, serverSocket, "client-to-server"));
189
+ serverSocket.on("data", this.makePipe(serverSocket, clientSocket, "server-to-client"));
190
+ }
191
+ /** Builds the `data`-event handler for one direction of one connection.
192
+ * Per-pipe state (the frame counter, the one-shot flags below) lives in
193
+ * this closure, not on the proxy or the connection entry, so concurrent
194
+ * connections and the two directions of the same connection never share
195
+ * counters. */
196
+ makePipe(from, to, direction) {
197
+ const counter = new WsFrameCounter();
198
+ let blackHoled = false;
199
+ let reorderPending = null;
200
+ // The very first bytes in either direction are the plain-text HTTP
201
+ // Upgrade request/response, not a WS frame at all — `WsFrameCounter`'s
202
+ // frame-header parser applied to arbitrary HTTP header bytes produces a
203
+ // garbage "frame length" that usually resolves within the header block
204
+ // itself, silently arming a fault (and, for `drop-no-fin`/`stall-reads`,
205
+ // swallowing the 101 response before the client ever sees it) during
206
+ // the handshake instead of the application traffic the fault is meant to
207
+ // target. Every fault kind is a no-op (plain pass-through, no counting)
208
+ // until this direction's own handshake terminator (`\r\n\r\n`) has been
209
+ // seen; a connection whose handshake never terminates within 16 KiB
210
+ // (never happens for this harness's own client/server) falls back to
211
+ // fault-active rather than hanging the scan forever.
212
+ let handshakeDone = false;
213
+ let handshakeScan = Buffer.alloc(0);
214
+ return (chunk) => {
215
+ if (blackHoled)
216
+ return;
217
+ if (!handshakeDone) {
218
+ to.write(chunk);
219
+ handshakeScan =
220
+ handshakeScan.length > 0 ? Buffer.concat([handshakeScan, chunk]) : chunk;
221
+ if (handshakeScan.includes("\r\n\r\n") || handshakeScan.length > 16384) {
222
+ handshakeDone = true;
223
+ }
224
+ return;
225
+ }
226
+ const fault = this.fault;
227
+ if (!fault || direction !== relevantDirection(fault.kind)) {
228
+ to.write(chunk);
229
+ return;
230
+ }
231
+ counter.feed(chunk);
232
+ const armed = counter.count > (fault.afterFrames ?? 0);
233
+ switch (fault.kind) {
234
+ case "drop-no-fin":
235
+ // Half-open: once armed, never forward and never close either
236
+ // socket — a live-looking connection nothing moves on.
237
+ if (armed) {
238
+ blackHoled = true;
239
+ return;
240
+ }
241
+ to.write(chunk);
242
+ return;
243
+ case "abrupt-close":
244
+ // Forward the frame that arms the fault (the run_job command
245
+ // itself must actually reach the server), then hang up hard —
246
+ // no FIN, no close frame — right after.
247
+ to.write(chunk);
248
+ if (armed) {
249
+ blackHoled = true;
250
+ from.destroy();
251
+ to.destroy();
252
+ }
253
+ return;
254
+ case "stall-reads":
255
+ // Stop reading from `from` (the server side) once armed — its
256
+ // socket buffer, then its own send buffer, backs up because
257
+ // nothing drains it, which is the drain-timeout path's trigger.
258
+ // The client-facing socket is untouched: any already-forwarded
259
+ // bytes remain delivered.
260
+ if (armed) {
261
+ blackHoled = true;
262
+ from.pause();
263
+ return;
264
+ }
265
+ to.write(chunk);
266
+ return;
267
+ case "delay":
268
+ setTimeout(() => {
269
+ if (!to.destroyed)
270
+ to.write(chunk);
271
+ }, fault.delayMs ?? 200);
272
+ return;
273
+ case "fragment":
274
+ writeFragmented(to, chunk, fault.chunkBytes ?? 4);
275
+ return;
276
+ case "reorder":
277
+ // Swap exactly one adjacent pair once armed, then resume in-order
278
+ // forwarding — enough to exercise a client's/server's tolerance
279
+ // for a single out-of-order delivery without permanently
280
+ // reordering the whole stream.
281
+ if (!armed) {
282
+ to.write(chunk);
283
+ return;
284
+ }
285
+ if (reorderPending === null) {
286
+ reorderPending = chunk;
287
+ return;
288
+ }
289
+ // Synchronous double-write, not `setImmediate`-deferred — same
290
+ // interleaving hazard `writeFragmented` has: a deferred write can
291
+ // still be pending when the next `data` event's own writes land,
292
+ // putting bytes on `to` out of the order this fault itself
293
+ // intended.
294
+ {
295
+ const previous = reorderPending;
296
+ reorderPending = null;
297
+ to.write(chunk);
298
+ to.write(previous);
299
+ }
300
+ return;
301
+ }
302
+ };
303
+ }
304
+ }
305
+ //# sourceMappingURL=net-proxy.js.map