tropel-engine 0.6.0

The tropel load-test engine: scenario execution, VU scheduling, the protocol registry and the metrics pipeline.
Documentation
{
  "$comment": "TR-465 / KT-404 \u2014 what a user script can rely on in each realm. MEASURED, not asserted from documentation: every value here came from running the probe in that realm and recording the answer. The point is that the divergence between KnockPort's host-JS realm (new Function + eval, the browser and the web app) and tropel's QuickJS realm (load runs, and desktop from KT-404 on) becomes a committed table instead of folklore. A probe whose answer changes fails the test in BOTH repos until this file is updated, so the surface cannot drift silently.",
  "$realms": {
    "hostJs": "KnockPort's scripting realm \u2014 `createRealmWithHost` in packages/engine/src/scripting-core.ts. The page's own engine.",
    "quickJs": "tropel's script realm \u2014 the shim bundle over tropel-js, used by every load run and by POST /script."
  },
  "probes": [
    {
      "name": "fetch",
      "expression": "typeof fetch",
      "hostJs": "function",
      "quickJs": "function",
      "why": "CLOSED by TR-475. The QuickJS realm now has `fetch`, bridged to the SAME host client `pm.sendRequest` rides \u2014 one network path out of the realm, not two that can disagree. Kept as a probe precisely because the two realms AGREEING here is a property worth pinning; it was a divergence until it was measured and fixed."
    },
    {
      "name": "crypto",
      "expression": "typeof crypto",
      "hostJs": "object",
      "quickJs": "undefined",
      "why": "No WebCrypto in QuickJS. Signing belongs to tropel-auth, reached through the shims; a script reaching for crypto directly is reaching past the seam."
    },
    {
      "name": "crypto.randomUUID",
      "expression": "typeof crypto !== 'undefined' && typeof crypto.randomUUID",
      "hostJs": "function",
      "quickJs": "false",
      "why": "The commonest way a script mints a request id. Use {{$guid}} or pm.variables \u2014 the dynamic catalogue is the same Rust in both realms."
    },
    {
      "name": "TextEncoder",
      "expression": "typeof TextEncoder",
      "hostJs": "function",
      "quickJs": "undefined",
      "why": "A browser global."
    },
    {
      "name": "Intl",
      "expression": "typeof Intl",
      "hostJs": "object",
      "quickJs": "undefined",
      "why": "No Intl in QuickJS, so toLocaleString and friends format differently. Date and number formatting in a script is a real divergence, not a theoretical one."
    },
    {
      "name": "setTimeout",
      "expression": "typeof setTimeout",
      "hostJs": "function",
      "quickJs": "undefined",
      "why": "There is no event loop to schedule onto in the script realm. A script that awaits a timer hangs in the app and throws under load."
    },
    {
      "name": "structuredClone",
      "expression": "typeof structuredClone",
      "hostJs": "function",
      "quickJs": "undefined",
      "why": "A browser global."
    },
    {
      "name": "URL",
      "expression": "typeof URL",
      "hostJs": "function",
      "quickJs": "undefined",
      "why": "URL parsing in a script diverges. The resolver's own URL handling is tropel's and is identical in both realms."
    },
    {
      "name": "error.stack contains the message",
      "expression": "(() => { try { null.x; } catch (e) { return String(e.stack).indexOf(String(e.message)) >= 0 ? 'yes' : 'no'; } })()",
      "hostJs": "yes",
      "quickJs": "no",
      "why": "QuickJS's stack carries frames only. `e.stack || e.message` therefore LOSES the message in one realm and not the other \u2014 this exact shape swallowed a script error during KT-203 and is why the agent reports message-first."
    },
    {
      "name": "localStorage",
      "expression": "typeof localStorage",
      "hostJs": "undefined",
      "quickJs": "undefined",
      "why": "Absent in BOTH. Recorded because agreeing is worth pinning too: this is not a divergence and must not become one."
    },
    {
      "name": "console",
      "expression": "typeof console",
      "hostJs": "object",
      "quickJs": "object",
      "why": "Both realms install console."
    },
    {
      "name": "btoa",
      "expression": "typeof btoa",
      "hostJs": "function",
      "quickJs": "function",
      "why": "Both."
    },
    {
      "name": "BigInt",
      "expression": "typeof BigInt",
      "hostJs": "function",
      "quickJs": "function",
      "why": "Both."
    },
    {
      "name": "Array.prototype.at",
      "expression": "typeof [].at",
      "hostJs": "function",
      "quickJs": "function",
      "why": "ES2022 \u2014 both."
    },
    {
      "name": "String.prototype.replaceAll",
      "expression": "typeof ''.replaceAll",
      "hostJs": "function",
      "quickJs": "function",
      "why": "ES2021 \u2014 both."
    },
    {
      "name": "Object.hasOwn",
      "expression": "typeof Object.hasOwn",
      "hostJs": "function",
      "quickJs": "function",
      "why": "ES2022 \u2014 both."
    },
    {
      "name": "Promise.allSettled",
      "expression": "typeof Promise.allSettled",
      "hostJs": "function",
      "quickJs": "function",
      "why": "ES2020 \u2014 both."
    },
    {
      "name": "regexp lookbehind",
      "expression": "(() => { try { new RegExp('(?<=a)b'); return 'ok'; } catch (e) { return 'unsupported'; } })()",
      "hostJs": "ok",
      "quickJs": "ok",
      "why": "QuickJS's libregexp supports lookbehind. Worth pinning: the regex engines genuinely differ, so agreement here is a measurement, not an assumption."
    },
    {
      "name": "regexp named groups",
      "expression": "(() => { try { return 'x'.match(/(?<n>x)/).groups.n; } catch (e) { return 'unsupported'; } })()",
      "hostJs": "x",
      "quickJs": "x",
      "why": "Both."
    },
    {
      "name": "pm",
      "expression": "typeof pm",
      "hostJs": "object",
      "quickJs": "object",
      "why": "The Postman-compat surface, in both."
    },
    {
      "name": "bru",
      "expression": "typeof bru",
      "hostJs": "object",
      "quickJs": "object",
      "why": "The Bruno-compat surface. This probe FOUND A BUG: POST /script hand-rolled its shim list (k6-core + pm only) instead of using ShimBundle::default(), so `bru` was undefined on the agent and an object in the app \u2014 a Bruno-style script worked in KnockPort and failed on desktop. js_bootstrap.rs exists because that exact defect happened once before ('bru.js was compiled into the binary but NEVER evaluated'); re-deriving the list in agent.rs re-opened it one endpoint over. Fixed in TR-465."
    }
  ]
}