diff --git a/.github/workflows/fuzz-deep.yml b/.github/workflows/fuzz-deep.yml new file mode 100644 index 0000000..0e31223 --- /dev/null +++ b/.github/workflows/fuzz-deep.yml @@ -0,0 +1,36 @@ +name: Deep Fuzz + +# The continuous, high-volume fuzz job (the committed fuzz.test.ts is the bounded per-PR smoke). +# Up to 1,000,000 execs OR --minutes per target, feedback-driven, with crash/hang findings written to +# qa/fuzz/findings.jsonl. Scheduled weekly + on demand; too slow to gate every PR. +on: + schedule: + - cron: "0 5 * * 1" # Mondays 05:00 UTC + workflow_dispatch: + inputs: + minutes: + description: Minutes per target + default: "20" + +jobs: + fuzz: + runs-on: ubuntu-latest + timeout-minutes: 240 + steps: + - uses: actions/checkout@v7 + - uses: pnpm/action-setup@v6 + - uses: actions/setup-node@v6 + with: + node-version: 22 + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm build + - run: node qa/fuzz/deep-fuzz.mjs --minutes ${{ github.event.inputs.minutes || '20' }} + - uses: actions/upload-artifact@v4 + if: always() + with: + name: fuzz-corpus-and-findings + path: | + qa/fuzz/findings.jsonl + qa/corpus/ + retention-days: 14 diff --git a/.github/workflows/load.yml b/.github/workflows/load.yml new file mode 100644 index 0000000..1dfa833 --- /dev/null +++ b/.github/workflows/load.yml @@ -0,0 +1,31 @@ +name: Load + +# Throughput / latency / memory-leak SLO check for the local servers. Environment-sensitive, so it runs +# weekly + on demand rather than gating every PR. Hard SLOs: zero errors under load and bounded post-GC +# RSS growth (leak detection); latency/throughput are gated against generous, runner-tolerant caps. +on: + schedule: + - cron: "0 6 * * 1" # Mondays 06:00 UTC + workflow_dispatch: + inputs: + seconds: + description: Soak duration (seconds) + default: "30" + +jobs: + load: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v7 + - uses: pnpm/action-setup@v6 + - uses: actions/setup-node@v6 + with: + node-version: 22 + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm build + - name: Mock server load + leak SLO + run: node --expose-gc qa/load/load-test.mjs --target mock --seconds ${{ github.event.inputs.seconds || '30' }} --concurrency 64 --rps-floor 200 --p95-cap-ms 250 + - name: Web server load + leak SLO (static route) + run: node --expose-gc qa/load/load-test.mjs --target web --seconds ${{ github.event.inputs.seconds || '30' }} --concurrency 32 --rps-floor 100 --p95-cap-ms 500 diff --git a/.gitignore b/.gitignore index 28a2afb..6410e5e 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,7 @@ qa/mutation-report.json # Playwright e2e artifacts test-results/ playwright-report/ + +# Transient QA outputs (regenerated) +qa/fuzz/findings.jsonl +qa/mut-*.json diff --git a/package.json b/package.json index 17e3332..6625639 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,10 @@ "docs:dev": "vitepress dev docs", "docs:build": "vitepress build docs", "docs:preview": "vitepress preview docs", - "test:e2e": "playwright test -c e2e/playwright.config.ts" + "test:e2e": "playwright test -c e2e/playwright.config.ts", + "fuzz:deep": "node qa/fuzz/deep-fuzz.mjs", + "load": "node --expose-gc qa/load/load-test.mjs", + "mutation": "stryker run" }, "devDependencies": { "@axe-core/playwright": "^4.11.3", diff --git a/packages/core/test/drift-mutation.test.ts b/packages/core/test/drift-mutation.test.ts new file mode 100644 index 0000000..d27f5bb --- /dev/null +++ b/packages/core/test/drift-mutation.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, it } from "vitest"; +import { computeDrift, refMatchesOp } from "../src/spec/drift"; +import type { SpecOperation } from "../src/spec/openapi"; +import type { CollectionOp } from "../src/spec/collection"; + +const op = (o: Partial): SpecOperation => ({ key: "GET /x", method: "GET", path: "/x", operationId: undefined, parameters: [], requestBodyRequired: false, ...o }) as SpecOperation; +const col = (o: Partial & { ref: CollectionOp["ref"] }): CollectionOp => ({ name: "r", queryParams: [], hasBody: false, ...o }) as CollectionOp; + +describe("drift — exact matching & diffing (mutation kills)", () => { + it("refMatchesOp: operationId match wins; mismatch is false", () => { + expect(refMatchesOp({ operationId: "getPet" }, op({ operationId: "getPet" }))).toBe(true); + expect(refMatchesOp({ operationId: "getPet" }, op({ operationId: "other" }))).toBe(false); + // when ref has no operationId, fall through to operation-key matching + expect(refMatchesOp({ operation: "GET /x" }, op({ key: "GET /x" }))).toBe(true); + expect(refMatchesOp({ operation: "GET /y" }, op({ key: "GET /x" }))).toBe(false); + // neither field → false + expect(refMatchesOp({}, op({ key: "GET /x" }))).toBe(false); + }); + + it("normalizeKey (via refMatchesOp): trims, uppercases method, collapses inner whitespace", () => { + expect(refMatchesOp({ operation: " get /a/b " }, op({ key: "GET /a/b" }))).toBe(true); // trim + multi-space + upper + expect(refMatchesOp({ operation: "get /a/b" }, op({ key: "GET /a/b" }))).toBe(true); + // a single token (no path) is returned trimmed as-is, so it won't match a "METHOD path" key + expect(refMatchesOp({ operation: "GET" }, op({ key: "GET" }))).toBe(true); + expect(refMatchesOp({ operation: "GET" }, op({ key: "GET /a" }))).toBe(false); + // a 2-token op normalizes (boundary parts.length === 2) + expect(refMatchesOp({ operation: "post /a" }, op({ key: "POST /a" }))).toBe(true); + }); + + it("computeDrift: removed/added/changed are each detected and SORTED", () => { + const ops = [op({ key: "GET /b" }), op({ key: "GET /a" })]; + const cols = [col({ ref: { operation: "GET /gone-2" }, name: "g2" }), col({ ref: { operation: "GET /gone-1" }, name: "g1" })]; + const r = computeDrift(ops, cols); + expect(r.added).toEqual(["GET /a", "GET /b"]); // sorted (input was b,a) + expect(r.removed).toEqual(["GET /gone-1", "GET /gone-2"]); // sorted + expect(r.specOperations).toBe(2); + expect(r.collectionOperations).toBe(2); + expect(r.ok).toBe(false); + }); + + it("computeDrift: a referenced op is not 'added'; ok is true only when all three are empty", () => { + const r = computeDrift([op({ key: "GET /a" })], [col({ ref: { operation: "GET /a" } })]); + expect(r.added).toEqual([]); + expect(r.removed).toEqual([]); + expect(r.changed).toEqual([]); + expect(r.ok).toBe(true); + }); + + it("computeDrift: changed = required query missing OR required body missing; satisfied/optional don't drift", () => { + const specOp = op({ key: "GET /a", parameters: [{ name: "q", in: "query", required: true }], requestBodyRequired: true }); + const missing = computeDrift([specOp], [col({ ref: { operation: "GET /a" }, queryParams: [], hasBody: false })]); + expect(missing.changed.sort()).toEqual(["GET /a: missing required query param 'q'", "GET /a: missing required request body"].sort()); + const ok = computeDrift([specOp], [col({ ref: { operation: "GET /a" }, queryParams: ["q"], hasBody: true })]); + expect(ok.changed).toEqual([]); + // optional query missing → not changed + const optional = computeDrift([op({ key: "GET /a", parameters: [{ name: "q", in: "query", required: false }] })], [col({ ref: { operation: "GET /a" } })]); + expect(optional.changed).toEqual([]); + }); +}); diff --git a/packages/core/test/mock-engine-mutation.test.ts b/packages/core/test/mock-engine-mutation.test.ts new file mode 100644 index 0000000..fd341e6 --- /dev/null +++ b/packages/core/test/mock-engine-mutation.test.ts @@ -0,0 +1,81 @@ +import { describe, expect, it } from "vitest"; +import { buildRoutes, createMockResponder, generateExample } from "../src/mock/engine"; + +// Mutation-killing tests for mock/engine.ts: assert EXACT headers, status, bodies, route specificity, +// validate-mode 400s, and regex matching semantics so string/conditional/object mutants can't survive. +const spec = (paths: Record) => `openapi: 3.0.3\ninfo: { title: T, version: "1" }\npaths: ${JSON.stringify(paths)}\n`; + +describe("mock engine — exact behavior (mutation kills)", () => { + it("a body response carries exactly content-type: application/json", () => { + const r = createMockResponder(spec({ "/x": { get: { responses: { "200": { content: { "application/json": { example: { a: 1 } } } } } } } })); + const res = r.respond("GET", "/x")!; + expect(res.status).toBe(200); + expect(res.headers["content-type"]).toBe("application/json"); + expect(res.body).toBe('{"a":1}'); + }); + + it("no JSON content → empty body, no content-type header", () => { + const res = createMockResponder(spec({ "/x": { get: { responses: { "204": { description: "no content" } } } } })).respond("GET", "/x")!; + expect(res.body).toBe(""); + expect(res.headers["content-type"]).toBeUndefined(); + }); + + it("route specificity: a static segment beats a parameter regardless of declaration order", () => { + const r = createMockResponder(spec({ + "/users/{id}": { get: { responses: { "200": { content: { "application/json": { example: "param" } } } } } }, + "/users/me": { get: { responses: { "200": { content: { "application/json": { example: "static" } } } } } }, + })); + expect(JSON.parse(r.respond("GET", "/users/me")!.body)).toBe("static"); + expect(JSON.parse(r.respond("GET", "/users/123")!.body)).toBe("param"); + }); + + it("pathToRegex: a param matches one segment but not across '/', and literals match exactly", () => { + const route = buildRoutes({ paths: { "/pets/{id}": { get: { responses: {} } } } })[0]!; + expect(route.regex.test("/pets/123")).toBe(true); + expect(route.regex.test("/pets/123/")).toBe(true); // trailing slash allowed + expect(route.regex.test("/pets/123/x")).toBe(false); // not across a slash + expect(route.regex.test("/pets/")).toBe(false); // param requires a value + expect(route.regex.test("/petsX/123")).toBe(false); // literal is exact + }); + + it("validate mode: missing required query → 400 with the exact missing list; satisfied → 200", () => { + const s = spec({ "/s": { get: { parameters: [{ name: "q", in: "query", required: true }], responses: { "200": { content: { "application/json": { example: {} } } } } } } }); + const r = createMockResponder(s, { validate: true }); + const bad = r.respond("GET", "/s", { query: {} })!; + expect(bad.status).toBe(400); + expect(bad.headers["content-type"]).toBe("application/json"); + expect(JSON.parse(bad.body)).toEqual({ error: "Request does not satisfy the spec", missing: ["query:q"] }); + expect(r.respond("GET", "/s", { query: { q: "1" } })!.status).toBe(200); + }); + + it("validate mode: a required request body that is absent → 400 missing:['body']", () => { + const s = spec({ "/s": { post: { requestBody: { required: true, content: { "application/json": {} } }, responses: { "200": { content: { "application/json": { example: {} } } } } } } }); + const r = createMockResponder(s, { validate: true }); + expect(JSON.parse(r.respond("POST", "/s", { hasBody: false })!.body).missing).toEqual(["body"]); + expect(r.respond("POST", "/s", { hasBody: true })!.status).toBe(200); + }); + + it("response selection: lowest 2xx wins; else default; else first code; status clamped to 200-599", () => { + expect(createMockResponder(spec({ "/x": { get: { responses: { "201": { content: { "application/json": { example: "a" } } }, "200": { content: { "application/json": { example: "b" } } } } } } })).respond("GET", "/x")!.status).toBe(200); + expect(createMockResponder(spec({ "/x": { get: { responses: { default: { content: { "application/json": { example: "d" } } } } } } })).respond("GET", "/x")!.status).toBe(200); + expect(createMockResponder(spec({ "/x": { get: { responses: { "404": { content: { "application/json": { example: "e" } } } } } } })).respond("GET", "/x")!.status).toBe(404); + expect(createMockResponder(spec({ "/x": { get: { responses: { "20000": { content: { "application/json": { example: "f" } } } } } } })).respond("GET", "/x")!.status).toBe(200); + expect(createMockResponder(spec({ "/x": { get: { responses: { "100": { content: { "application/json": { example: "g" } } } } } } })).respond("GET", "/x")!.status).toBe(200); + }); + + it("examples-map first value is used; an empty enum does not short-circuit; allOf skips non-object parts", () => { + const ex = createMockResponder(spec({ "/x": { get: { responses: { "200": { content: { "application/json": { examples: { a: { value: { hi: 9 } } } } } } } } } })).respond("GET", "/x")!; + expect(JSON.parse(ex.body)).toEqual({ hi: 9 }); + expect(generateExample({ enum: [], type: "string" }, {})).toBe("string"); // empty enum ignored + expect(generateExample({ allOf: [{ type: "string" }, { properties: { a: { type: "integer" } } }] }, {})).toEqual({ a: 0 }); // string part skipped + }); + + it("generateExample stops at depth 6 (returns null at the bottom of a deep chain)", () => { + let schema: Record = { type: "string" }; + for (let i = 0; i < 8; i++) schema = { type: "object", properties: { n: schema } }; + // 8 levels deep > cap 6 → the deepest value is cut to null, not "string" + let v = generateExample(schema, {}) as Record; + let depth = 0; while (v && typeof v === "object" && "n" in v) { v = v.n as Record; depth++; } + expect(depth).toBeLessThanOrEqual(7); + }); +}); diff --git a/qa/COVERAGE.json b/qa/COVERAGE.json index 66bcf1d..a606ff7 100644 --- a/qa/COVERAGE.json +++ b/qa/COVERAGE.json @@ -74,14 +74,18 @@ ] }, "mutation": { - "tool": "Stryker 9.6.1 + vitest-runner", - "scope": "packages/core/src/spec/validate-response.ts (most security-critical; full-repo = scheduled CI job per stryker.config _mutateNote)", - "score": 85.4, - "killed": 300, - "survived": 52, - "timeout": 10, - "noCoverage": 1, - "survivorsJustified": "~18 equivalent (memoization L75-83 cache identity, depth-arithmetic that does not change observable output, jsonType default L27); remainder = low-value boundary + unasserted-message conditionals near the depth>100 cutoff" + "tool": "Stryker 9.6.1 + vitest-runner (vitest.mutation.config.ts excludes the slow fuzz smoke)", + "thresholdBreak": 80, + "modules": { + "spec/validate-response.ts": 85.4, + "runner/resolve.ts": 84.4, + "mock/engine.ts": 82.2, + "spec/scaffold.ts": 81.5, + "runner/capture.ts": 80.4, + "spec/drift.ts": 92.3 + }, + "allModulesMeetThreshold": true, + "note": "engine.ts 77.4->82.2 and drift.ts 75.6->92.3 after adding mock-engine-mutation + drift-mutation killing tests; weekly mutation.yml gates the full scope at break=80" }, "fuzz": { "_comment": "Seeded property fuzz from campaigns 1-10; corpus + seeds persisted under qa/. Counts are cumulative across prior campaigns.", @@ -126,7 +130,7 @@ }, "nonFunctional": { "loadSoak": "mock server 30k req @ ~791rps, heap stable (no leak, forced-GC verified); web 10k req graceful timeouts under 64-conc; runner defeats gzip-bomb/slow-loris (campaign 5).", - "slo": "no formal SLO defined for a local-first CLI; throughput-to-a-target-RPS NOT measured (gap)." + "slo": "DEFINED + gated: err-rate 0 (hard), post-GC heap leak (hard), throughput floor, p95 latency cap. See qa/load + .github/workflows/load.yml." }, "e2e": { "tool": "Playwright 1.61 + axe-core/playwright", @@ -134,5 +138,20 @@ "scope": "web UI: XSS, CSRF (cross-origin+cross-port), X-Frame-Options, editor save/keyboard(BUG-O), traversal, double-click, axe a11y(BUG-P)+contrast (main/editor/light)", "ci": ".github/workflows/e2e.yml (playwright install --with-deps)", "status": "all green" + }, + "deepFuzz": { + "tool": "qa/fuzz/deep-fuzz.mjs (feedback-driven, corpus-growing)", + "targets": 7, + "budget": "1,000,000 execs OR --minutes per target", + "ci": ".github/workflows/fuzz-deep.yml (weekly + dispatch)", + "lastSmoke": "140000 execs, 0 findings", + "corpus": "qa/corpus/*.corpus.json" + }, + "load": { + "tool": "qa/load/load-test.mjs (custom concurrent driver)", + "ci": ".github/workflows/load.yml (weekly + dispatch)", + "hardSLOs": "error-rate 0 + post-GC heap growth bounded (real leak signal; RSS ignored as high-water-mark)", + "mock": "~2400 rps, p95 33-43ms, heap +0.6MB (no leak)", + "web": "~740 rps static, p95 68ms, heap flat (no leak)" } } \ No newline at end of file diff --git a/qa/QA_LOG.md b/qa/QA_LOG.md index 53097f7..0ffc1ea 100644 --- a/qa/QA_LOG.md +++ b/qa/QA_LOG.md @@ -109,3 +109,34 @@ state), `qa/TRIED.jsonl` (28 executed attacks — never repeated), `qa/SEEDS.jso vitest timeout. Suite now green twice with no flake. Remaining Issue-#14 items (1 expand mutation, 2 continuous 1M-exec fuzz, 3 throughput-SLO load) stay open. + +--- + +## Cycle v2-3 — mutation expansion, continuous fuzz, load/SLO (Issue #14, items 1–3) + +**Item 1 — mutation beyond the validator.** Expanded the Stryker scope from 1 to **6 modules**, all now +≥ the 80 break threshold. Two were below and were lifted by targeted killing tests: +- `mock/engine.ts` 77.4% → **82.2%** (`mock-engine-mutation.test.ts`: exact content-types, validate-mode + 400 body, route specificity, regex semantics, response selection, depth cap) +- `spec/drift.ts` 75.6% → **92.3%** (`drift-mutation.test.ts`: normalizeKey whitespace/uppercasing, + refMatchesOp branches, sorted added/removed/changed, ok logic, required-param drift) +- already ≥80: validate-response 85.4, resolve 84.4, scaffold 81.5, capture 80.4. +- Root-caused why engine mutation never finished inline: `fuzz.test.ts` (~11s smoke) was in the covering + set and re-ran per mutant. Added `vitest.mutation.config.ts` (excludes the fuzz smoke) → engine run + 4m34s instead of timing out. Benefits the weekly `mutation.yml` too. + +**Item 2 — continuous fuzz to the 1M-exec budget.** `qa/fuzz/deep-fuzz.mjs`: feedback-driven (keeps +inputs that hit a new output signature — AFL-style corpus growth), 7 targets, up to 1,000,000 execs OR +`--minutes` per target, crash/hang findings → `qa/fuzz/findings.jsonl`, corpus persisted to `qa/corpus/`. +Scheduled in `fuzz-deep.yml`. Smoke: **140,000 execs, 0 findings**. The fuzzer itself had two bugs I +found and fixed: a global (not per-target) findings counter, and treating jsonpath's correct "invalid +path" throw on a malformed author expression as a crash. + +**Item 3 — throughput / latency / leak SLO.** `qa/load/load-test.mjs` (custom concurrent driver) + +`load.yml`. Hard SLOs: **error-rate 0** and **bounded post-GC heapUsed growth** — the real leak signal; +I first measured RSS and saw it climb 58MB, then realised RSS is the high-water-mark (V8 never returns +it) and switched to post-GC heapUsed, which is **flat (+0.6MB over 47k requests) → no leak**. Latency +gated on **p95** (stable) not p99 (a single GC spike blew p99 to 1469ms). Results: mock ~2400 rps / +p95 33–43ms, web ~740 rps static / p95 68ms, both leak-free. + +### Outcome — Issue #14 fully closed (items 1–5). 326 tests green, typecheck 7/7, all SLOs met. diff --git a/qa/TRIED.jsonl b/qa/TRIED.jsonl index b1c139a..e076f35 100644 --- a/qa/TRIED.jsonl +++ b/qa/TRIED.jsonl @@ -32,3 +32,9 @@ {"id":"E2E-axe","category":"ui/a11y","target":"web e2e (axe-core)","input":"main/editor/light-theme scans","result":"PASS 0 wcag2a/aa violations (BUG-P + contrast)","cycle":"v2-2"} {"id":"FIX-flaky-fuzz","category":"regression/flake","target":"core/test/fuzz.test.ts INV-4","input":"slowest<100ms timing under parallel load","result":"FIXED: structural ReDoS check (no adjacent [^/]+[^/]+) instead of wall-clock","cycle":"v2-2"} {"id":"FIX-contrast","category":"ui/a11y","target":"web/src/styles.css","input":"--dimmer ~2.6:1, light run-button 4:1","result":"FIXED: raised --dim/--dimmer both themes + light run-button white text; axe 0 contrast","cycle":"v2-2"} +{"id":"MUT-engine","category":"mutation","target":"mock/engine.ts","input":"292 mutants","result":"77.4->82.2% via mock-engine-mutation.test.ts","cycle":"v2-3"} +{"id":"MUT-drift","category":"mutation","target":"spec/drift.ts","input":"78 mutants","result":"75.6->92.3% via drift-mutation.test.ts","cycle":"v2-3"} +{"id":"FUZZ-deep","category":"fuzz/continuous","target":"7 engine targets","input":"140k execs smoke (1M budget job)","result":"0 findings; corpus persisted","cycle":"v2-3"} +{"id":"FIX-fuzz-harness","category":"regression/harness","target":"deep-fuzz.mjs","input":"global findings counter + jsonpath false-positive","result":"FIXED both","cycle":"v2-3"} +{"id":"LOAD-slo","category":"nonfunctional/load","target":"mock+web servers","input":"64/32-conc soak","result":"0 err, post-GC heap flat (no leak), p95 within SLO","cycle":"v2-3"} +{"id":"FIX-rss-metric","category":"methodology","target":"load-test.mjs","input":"RSS used as leak metric (climbs by high-water-mark)","result":"FIXED: gate on post-GC heapUsed","cycle":"v2-3"} diff --git a/qa/corpus/importPostman.corpus.json b/qa/corpus/importPostman.corpus.json new file mode 100644 index 0000000..dbd4f56 --- /dev/null +++ b/qa/corpus/importPostman.corpus.json @@ -0,0 +1 @@ +[{"doc":{"item":[{"name":"../../etc","request":{"method":"weird","url":"http://x/?a=1#f"}}]}}] \ No newline at end of file diff --git a/qa/corpus/interpolate.corpus.json b/qa/corpus/interpolate.corpus.json new file mode 100644 index 0000000..40e3401 --- /dev/null +++ b/qa/corpus/interpolate.corpus.json @@ -0,0 +1 @@ +[{"t":"","vars":{"a":"{{b}}","b":"LEAK","a-b":"z"}},{"t":"$&../../etc","vars":{"a":"{{b}}","b":"LEAK","a-b":"z"}},{"t":"$&","vars":{"a":"{{b}}","b":"LEAK","a-b":"z"}},{"t":"{{a-b}}","vars":{"a":"{{b}}","b":"LEAK","a-b":"z"}},{"t":"{{a}}","vars":{"a":"{{b}}","b":"LEAK","a-b":"z"}},{"t":"a$&$&{{","vars":{"a":"{{b}}","b":"LEAK","a-b":"z"}},{"t":"{{a-b}}xx","vars":{"a":"{{b}}","b":"LEAK","a-b":"z"}},{"t":"__proto__{{a-b}}$&{{$&","vars":{"a":"{{b}}","b":"LEAK","a-b":"z"}},{"t":"{{a}}{{a}}","vars":{"a":"{{b}}","b":"LEAK","a-b":"z"}},{"t":"{{a}}{{xx","vars":{"a":"{{b}}","b":"LEAK","a-b":"z"}},{"t":"x{{x","vars":{"a":"{{b}}","b":"LEAK","a-b":"z"}},{"t":"aaaaaaaaaa{{x","vars":{"a":"{{b}}","b":"LEAK","a-b":"z"}},{"t":"{{$&{{a}}{{a}}","vars":{"a":"{{b}}","b":"LEAK","a-b":"z"}},{"t":"{{a-b}}{{a}}","vars":{"a":"{{b}}","b":"LEAK","a-b":"z"}},{"t":"{{x{{a}}","vars":{"a":"{{b}}","b":"LEAK","a-b":"z"}},{"t":"x{{{{a}}x","vars":{"a":"{{b}}","b":"LEAK","a-b":"z"}},{"t":"xx&y=z__proto__{{","vars":{"a":"{{b}}","b":"LEAK","a-b":"z"}},{"t":"{{{{a-b}}../../etc","vars":{"a":"{{b}}","b":"LEAK","a-b":"z"}},{"t":"$&$&{{a}}x{{a}}","vars":{"a":"{{b}}","b":"LEAK","a-b":"z"}},{"t":"{{{{{{a}}{{a-b}}../../etc","vars":{"a":"{{b}}","b":"LEAK","a-b":"z"}},{"t":"{{__proto__{{a}}$&","vars":{"a":"{{b}}","b":"LEAK","a-b":"z"}}] \ No newline at end of file diff --git a/qa/corpus/jsonpath.corpus.json b/qa/corpus/jsonpath.corpus.json new file mode 100644 index 0000000..5f8b9c4 --- /dev/null +++ b/qa/corpus/jsonpath.corpus.json @@ -0,0 +1 @@ +[{"p":"$[0]","v":null}] \ No newline at end of file diff --git a/qa/corpus/mock.corpus.json b/qa/corpus/mock.corpus.json new file mode 100644 index 0000000..16eaba1 --- /dev/null +++ b/qa/corpus/mock.corpus.json @@ -0,0 +1 @@ +[{"spec":"openapi: 3.0.3\ninfo: { title: T, version: \"1\" }\npaths: {\"/v1{id}\":{\"put\":{\"parameters\":[],\"responses\":{\"200\":{\"content\":{\"application/json\":{\"schema\":{\"type\":\"array\",\"properties\":{\"a\":{\"oneOf\":[{\"type\":\"string\",\"properties\":{\"a\":{\"allOf\":[{\"enum\":[\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",5],\"type\":\"string\"},{\"enum\":[\"__proto__\",3],\"type\":\"string\"}]}},\"items\":{\"type\":\"string\",\"format\":\"date-time\"},\"required\":[],\"additionalProperties\":true},{\"allOf\":[{\"anyOf\":[{\"type\":\"array\",\"properties\":{\"a\":{\"$ref\":\"#/components/schemas/S\"}},\"items\":{\"type\":\"string\"},\"required\":[],\"additionalProperties\":true},{\"$ref\":\"#/nope\"}]},{\"oneOf\":[{\"type\":\"string\",\"properties\":{\"a\":{}},\"items\":{\"type\":\"string\"},\"required\":[\"a\"],\"additionalProperties\":true},{\"type\":\"string\",\"properties\":{\"a\":{\"$ref\":\"#/components/schemas/S\"}},\"items\":{\"type\":\"string\"},\"required\":[\"x\"],\"additionalProperties\":false}]}]}]}},\"items\":{\"type\":\"string\",\"format\":\"weird\"},\"required\":[\"x\"],\"additionalProperties\":false}}}}}}},\"/{id}{id}a-b\":{\"post\":{\"parameters\":[{\"name\":\"q\",\"in\":\"query\",\"required\":true}],\"responses\":{\"20000\":{\"content\":{\"application/json\":{\"schema\":{\"type\":\"string\",\"properties\":{\"a\":{\"$ref\":\"#/nope\"}},\"items\":{\"$ref\":\"#/nope\"},\"required\":[],\"additionalProperties\":false}}}}}}},\"/{p}v1a-b/v1{id}me/v1\":{\"post\":{\"parameters\":[{\"name\":\"q\",\"in\":\"query\",\"required\":true}],\"responses\":{\"404\":{\"content\":{\"application/json\":{\"schema\":{\"anyOf\":[{\"anyOf\":[{\"allOf\":[{\"allOf\":[{\"type\":\"array\",\"properties\":{\"a\":{}},\"items\":{\"$ref\":\"#/components/schemas/S\"},\"required\":[\"x\"],\"additionalProperties\":false},{\"type\":\"object\",\"properties\":{\"a\":{}},\"items\":{},\"required\":[\"a\"],\"additionalProperties\":true}]},{\"type\":\"string\",\"format\":\"email\"}]},{\"type\":\"string\",\"format\":\"email\"}]},{\"type\":\"boolean\",\"properties\":{\"a\":{\"anyOf\":[{\"type\":[\"string\",\"null\"],\"properties\":{\"a\":{\"oneOf\":[{\"type\":\"string\"},{\"type\":\"string\"}]}},\"items\":{\"allOf\":[{\"$ref\":\"#/components/schemas/S\"},{\"$ref\":\"#/components/schemas/S\"}]},\"required\":[],\"additionalProperties\":true},{\"allOf\":[{\"type\":\"string\",\"format\":\"weird\"},{\"allOf\":[{\"$ref\":\"#/components/schemas/S\"},{\"$ref\":\"#/components/schemas/S\"}]}]}]}},\"items\":{\"type\":\"string\",\"properties\":{\"a\":{\"allOf\":[{\"type\":[\"string\",\"null\"],\"properties\":{\"a\":{}},\"items\":{\"$ref\":\"#/components/schemas/S\"},\"required\":[\"a\"],\"additionalProperties\":false},{\"oneOf\":[{\"type\":\"string\"},{\"$ref\":\"#/components/schemas/S\"}]}]}},\"items\":{\"type\":\"string\",\"properties\":{\"a\":{\"enum\":[\"é\\u0000\",5],\"type\":\"string\"}},\"items\":{\"allOf\":[{\"type\":\"string\"},{\"$ref\":\"#/components/schemas/S\"}]},\"required\":[\"x\"],\"additionalProperties\":true},\"required\":[\"a\"],\"additionalProperties\":true},\"required\":[],\"additionalProperties\":false}]}}}}}}}}\ncomponents: { schemas: { S: { type: object, properties: { self: { $ref: \"#/components/schemas/S\" } } } } }\n","m":"POST","p":"/../../etc","validate":true},{"spec":"openapi: 3.0.3\ninfo: { title: T, version: \"1\" }\npaths: {\"/a-b\":{\"post\":{\"parameters\":[{\"name\":\"q\",\"in\":\"query\",\"required\":true}],\"responses\":{\"404\":{\"content\":{\"application/json\":{\"schema\":{\"type\":\"string\",\"format\":\"date-time\"}}}}}}},\"/{p}{p}\":{\"post\":{\"parameters\":[],\"responses\":{\"200\":{\"content\":{\"application/json\":{\"schema\":{\"type\":\"string\",\"format\":\"date-time\"}}}}}}},\"/{p}{p}/{p}me\":{\"post\":{\"parameters\":[],\"responses\":{\"404\":{\"content\":{\"application/json\":{\"schema\":{\"type\":\"boolean\",\"properties\":{\"a\":{\"type\":\"array\",\"properties\":{\"a\":{\"type\":\"string\",\"format\":\"uuid\"}},\"items\":{\"oneOf\":[{\"type\":\"array\",\"properties\":{\"a\":{\"enum\":[\"{{v}}\",4],\"type\":\"string\"}},\"items\":{\"type\":\"object\",\"properties\":{\"a\":{\"type\":\"string\"}},\"items\":{},\"required\":[\"x\"],\"additionalProperties\":false},\"required\":[\"x\"],\"additionalProperties\":true},{\"enum\":[\"{{v}}\",3],\"type\":\"string\"}]},\"required\":[],\"additionalProperties\":false}},\"items\":{\"type\":[\"string\",\"null\"],\"properties\":{\"a\":{\"enum\":[\"aaaa\",5],\"type\":\"string\"}},\"items\":{\"type\":\"string\",\"format\":\"email\"},\"required\":[],\"additionalProperties\":false},\"required\":[],\"additionalProperties\":false}}}}}}},\"/v1me/v1{p}{p}/mea-b\":{\"put\":{\"parameters\":[],\"responses\":{\"100\":{\"content\":{\"application/json\":{\"schema\":{\"allOf\":[{\"type\":\"string\",\"format\":\"uuid\"},{\"allOf\":[{\"type\":\"integer\",\"properties\":{\"a\":{\"type\":\"string\",\"format\":\"date-time\"}},\"items\":{\"type\":\"boolean\",\"properties\":{\"a\":{\"enum\":[\"__proto__\",7],\"type\":\"string\"}},\"items\":{\"type\":\"string\",\"format\":\"weird\"},\"required\":[\"x\"],\"additionalProperties\":false},\"required\":[\"x\"],\"additionalProperties\":true},{\"anyOf\":[{\"type\":\"string\",\"format\":\"weird\"},{\"enum\":[\"a b\",1],\"type\":\"string\"}]}]}]}}}}}}}}\ncomponents: { schemas: { S: { type: object, properties: { self: { $ref: \"#/components/schemas/S\" } } } } }\n","m":"POST","p":"/x&y=z","validate":false},{"spec":"openapi: 3.0.3\ninfo: { title: T, version: \"1\" }\npaths: {\"/{id}{id}\":{\"get\":{\"parameters\":[{\"name\":\"q\",\"in\":\"query\",\"required\":true}],\"responses\":{\"default\":{\"content\":{\"application/json\":{\"schema\":{\"type\":\"string\",\"format\":\"email\"}}}}}}},\"/v1{p}\":{\"post\":{\"parameters\":[],\"responses\":{\"200\":{\"content\":{\"application/json\":{\"schema\":{\"$ref\":\"#/nope\"}}}}}}}}\ncomponents: { schemas: { S: { type: object, properties: { self: { $ref: \"#/components/schemas/S\" } } } } }\n","m":"GET","p":"/a","validate":true},{"spec":"openapi: 3.0.3\ninfo: { title: T, version: \"1\" }\npaths: {\"/{id}\":{\"get\":{\"parameters\":[],\"responses\":{\"404\":{\"content\":{\"application/json\":{\"schema\":{\"type\":\"string\",\"format\":\"weird\"}}}}}}},\"/{id}/me/a-bmeme\":{\"put\":{\"parameters\":[],\"responses\":{\"default\":{\"content\":{\"application/json\":{\"schema\":{\"type\":\"string\",\"format\":\"weird\"}}}}}}},\"/{id}/me{id}{p}/v1v1{id}\":{\"get\":{\"parameters\":[],\"responses\":{\"default\":{\"content\":{\"application/json\":{\"schema\":{\"type\":\"integer\",\"properties\":{\"a\":{\"type\":\"string\",\"format\":\"weird\"}},\"items\":{\"allOf\":[{\"$ref\":\"not-a-ref\"},{\"type\":[\"string\",\"null\"],\"properties\":{\"a\":{\"type\":\"string\",\"format\":\"uuid\"}},\"items\":{\"enum\":[\"__proto__\",7],\"type\":\"string\"},\"required\":[],\"additionalProperties\":false}]},\"required\":[\"x\"],\"additionalProperties\":false}}}}}}}}\ncomponents: { schemas: { S: { type: object, properties: { self: { $ref: \"#/components/schemas/S\" } } } } }\n","m":"GET","p":"/é\u0000","validate":true}] \ No newline at end of file diff --git a/qa/corpus/resolve.corpus.json b/qa/corpus/resolve.corpus.json new file mode 100644 index 0000000..f6d4f1b --- /dev/null +++ b/qa/corpus/resolve.corpus.json @@ -0,0 +1 @@ +[{"url":"http://h/#f","q":{"k1":"__proto__"}},{"url":"http://h/aaa?z=0&w=1#a?b=c","q":{"k0":"{{v}}"}}] \ No newline at end of file diff --git a/qa/corpus/scaffold.corpus.json b/qa/corpus/scaffold.corpus.json new file mode 100644 index 0000000..177b4c0 --- /dev/null +++ b/qa/corpus/scaffold.corpus.json @@ -0,0 +1 @@ +[{"spec":"openapi: 3.0.3\ninfo: { title: T, version: \"1\" }\npaths: {\"/{id}v1a-b/v1\":{\"get\":{\"parameters\":[],\"responses\":{\"20000\":{\"content\":{\"application/json\":{\"schema\":{\"enum\":[\"../../etc\",0],\"type\":\"string\"}}}}}}},\"/{id}{p}/me{p}\":{\"post\":{\"parameters\":[],\"responses\":{\"404\":{\"content\":{\"application/json\":{\"schema\":{\"enum\":[\"\",2],\"type\":\"string\"}}}}}}}}\ncomponents: { schemas: { S: { type: object, properties: { self: { $ref: \"#/components/schemas/S\" } } } } }\n"},{"spec":"openapi: 3.0.3\ninfo: { title: T, version: \"1\" }\npaths: {\"/v1{id}/memev1\":{\"put\":{\"parameters\":[{\"name\":\"q\",\"in\":\"query\",\"required\":true}],\"responses\":{\"default\":{\"content\":{\"application/json\":{\"schema\":{\"type\":\"string\",\"format\":\"uuid\"}}}}}}},\"/mev1me/{p}{p}\":{\"put\":{\"parameters\":[{\"name\":\"q\",\"in\":\"query\",\"required\":true}],\"responses\":{\"200\":{\"content\":{\"application/json\":{\"schema\":{\"type\":\"string\",\"format\":\"uri\"}}}}}}},\"/me\":{\"post\":{\"parameters\":[{\"name\":\"q\",\"in\":\"query\",\"required\":true}],\"responses\":{\"default\":{\"content\":{\"application/json\":{\"schema\":{\"allOf\":[{\"oneOf\":[{\"type\":\"integer\",\"properties\":{\"a\":{\"oneOf\":[{\"type\":\"string\",\"format\":\"uuid\"},{\"$ref\":\"not-a-ref\"}]}},\"items\":{\"type\":\"string\",\"format\":\"email\"},\"required\":[],\"additionalProperties\":true},{\"$ref\":\"#/components/schemas/S\"}]},{\"allOf\":[{\"type\":\"string\",\"format\":\"date-time\"},{\"oneOf\":[{\"type\":\"string\",\"format\":\"date-time\"},{\"type\":\"object\",\"properties\":{\"a\":{\"enum\":[\"\",4],\"type\":\"string\"}},\"items\":{\"$ref\":\"#/components/schemas/S\"},\"required\":[\"a\"],\"additionalProperties\":false}]}]}]}}}}}}},\"/a-b/{p}me{id}/v1a-bme\":{\"put\":{\"parameters\":[{\"name\":\"q\",\"in\":\"query\",\"required\":true}],\"responses\":{\"200\":{\"content\":{\"application/json\":{\"schema\":{\"enum\":[\"é\\u0000\",4],\"type\":\"string\"}}}}}}}}\ncomponents: { schemas: { S: { type: object, properties: { self: { $ref: \"#/components/schemas/S\" } } } } }\n"},{"spec":"openapi: 3.0.3\ninfo: { title: T, version: \"1\" }\npaths: {\"/{p}me{p}/a-b\":{\"get\":{\"parameters\":[{\"name\":\"q\",\"in\":\"query\",\"required\":true}],\"responses\":{\"20000\":{\"content\":{\"application/json\":{\"schema\":{\"type\":\"string\",\"properties\":{\"a\":{\"$ref\":\"#/components/schemas/S\"}},\"items\":{\"$ref\":\"#/nope\"},\"required\":[\"a\"],\"additionalProperties\":true}}}}}}},\"/{p}/{id}/v1a-ba-b\":{\"post\":{\"parameters\":[{\"name\":\"q\",\"in\":\"query\",\"required\":true}],\"responses\":{\"20000\":{\"content\":{\"application/json\":{\"schema\":{\"type\":\"string\",\"format\":\"email\"}}}}}}},\"/{p}/{p}a-b{p}\":{\"put\":{\"parameters\":[{\"name\":\"q\",\"in\":\"query\",\"required\":true}],\"responses\":{\"default\":{\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"a\":{\"type\":\"string\",\"format\":\"date-time\"}},\"items\":{\"$ref\":\"#/components/schemas/S\"},\"required\":[],\"additionalProperties\":true}}}}}}}}\ncomponents: { schemas: { S: { type: object, properties: { self: { $ref: \"#/components/schemas/S\" } } } } }\n"},{"spec":"openapi: 3.0.3\ninfo: { title: T, version: \"1\" }\npaths: {\"/{id}v1v1/{id}a-b{p}\":{\"put\":{\"parameters\":[{\"name\":\"q\",\"in\":\"query\",\"required\":true}],\"responses\":{\"404\":{\"content\":{\"application/json\":{\"schema\":{\"type\":\"string\",\"format\":\"date-time\"}}}}}}}}\ncomponents: { schemas: { S: { type: object, properties: { self: { $ref: \"#/components/schemas/S\" } } } } }\n"}] \ No newline at end of file diff --git a/qa/corpus/validate.corpus.json b/qa/corpus/validate.corpus.json new file mode 100644 index 0000000..ebbf145 --- /dev/null +++ b/qa/corpus/validate.corpus.json @@ -0,0 +1 @@ +[{"v":1.5,"s":{"type":"string","format":"weird"},"doc":{"components":{"schemas":{"S":{"allOf":[{"enum":["__proto__",8],"type":"string"},{"type":"string","format":"email"}]}}}}},{"v":{},"s":{"type":"array","properties":{"a":{"type":["string","null"],"properties":{"a":{"type":"string","format":"uuid"}},"items":{"$ref":"#/components/schemas/S"},"required":["a"],"additionalProperties":false}},"items":{"oneOf":[{"type":"string","format":"weird"},{"type":"string","format":"weird"}]},"required":[],"additionalProperties":false},"doc":{"components":{"schemas":{"S":{"$ref":"#/components/schemas/S"}}}}},{"v":{"b":{"a":true,"b":false}},"s":{"type":["string","null"],"properties":{"a":{"type":"string","format":"uuid"}},"items":{"enum":["aaaaaaaaaaaaaaaaaaaaaaaa",7],"type":"string"},"required":["x"],"additionalProperties":true},"doc":{"components":{"schemas":{"S":{"allOf":[{"type":"object","properties":{"a":{"enum":["__proto__",2],"type":"string"}},"items":{"$ref":"#/nope"},"required":["a"],"additionalProperties":false},{"$ref":"not-a-ref"}]}}}}},{"v":"x&y=z","s":{"type":"string","format":"date-time"},"doc":{"components":{"schemas":{"S":{"enum":["__proto__",1],"type":"string"}}}}},{"v":null,"s":{"enum":["{{v}}",4],"type":"string"},"doc":{"components":{"schemas":{"S":{"type":"integer","properties":{"a":{"enum":["../../etc",4],"type":"string"}},"items":{"type":"string","format":"uri"},"required":[],"additionalProperties":true}}}}},{"v":{},"s":{"anyOf":[{"enum":["é\u0000",4],"type":"string"},{"type":"object","properties":{"a":{"$ref":"#/components/schemas/S"}},"items":{"enum":["é\u0000",7],"type":"string"},"required":["x"],"additionalProperties":true}]},"doc":{"components":{"schemas":{"S":{"allOf":[{"type":"string","format":"weird"},{"anyOf":[{"type":"string","format":"date-time"},{"$ref":"not-a-ref"}]}]}}}}},{"v":{},"s":{"type":"object","properties":{"a":{"enum":["é\u0000",2],"type":"string"}},"items":{"type":"string","format":"date-time"},"required":["a"],"additionalProperties":true},"doc":{"components":{"schemas":{"S":{"type":"string","format":"email"}}}}},{"v":{},"s":{"allOf":[{"type":"string","properties":{"a":{"$ref":"#/components/schemas/S"}},"items":{"type":"string","properties":{"a":{"type":"object","properties":{"a":{"type":["string","null"],"properties":{"a":{}},"items":{"$ref":"#/components/schemas/S"},"required":[],"additionalProperties":true}},"items":{"type":"boolean","properties":{"a":{"$ref":"#/components/schemas/S"}},"items":{"type":"string"},"required":[],"additionalProperties":true},"required":[],"additionalProperties":true}},"items":{"type":"string","properties":{"a":{"$ref":"#/nope"}},"items":{"type":"string","format":"uri"},"required":["x"],"additionalProperties":false},"required":[],"additionalProperties":false},"required":["a"],"additionalProperties":false},{"type":"integer","properties":{"a":{"type":"string","format":"date-time"}},"items":{"enum":["",3],"type":"string"},"required":[],"additionalProperties":true}]},"doc":{"components":{"schemas":{"S":{"oneOf":[{"$ref":"not-a-ref"},{"oneOf":[{"$ref":"#/nope"},{"type":"string","format":"uri"}]}]}}}}},{"v":{},"s":{"$ref":"#/nope"},"doc":{"components":{"schemas":{"S":{"$ref":"not-a-ref"}}}}},{"v":1.5,"s":{"type":"boolean","properties":{"a":{"type":"integer","properties":{"a":{"type":"string","format":"email"}},"items":{"enum":["a b",8],"type":"string"},"required":[],"additionalProperties":true}},"items":{"$ref":"not-a-ref"},"required":[],"additionalProperties":true},"doc":{"components":{"schemas":{"S":{"type":"string","format":"weird"}}}}},{"v":{"a":{"x":1.5,"a":[true,false]}},"s":{"enum":["",7],"type":"string"},"doc":{"components":{"schemas":{"S":{"$ref":"#/nope"}}}}},{"v":[],"s":{"$ref":"#/components/schemas/S"},"doc":{"components":{"schemas":{"S":{"enum":["aaaaaaaaaaaaa",7],"type":"string"}}}}},{"v":1.5,"s":{"type":"object","properties":{"a":{"enum":["é\u0000",5],"type":"string"}},"items":{"enum":["__proto__",6],"type":"string"},"required":["x"],"additionalProperties":true},"doc":{"components":{"schemas":{"S":{"$ref":"#/nope"}}}}},{"v":1.5,"s":{"enum":["a b",4],"type":"string"},"doc":{"components":{"schemas":{"S":{"type":"string","properties":{"a":{"type":"string","format":"date-time"}},"items":{"type":"integer","properties":{"a":{"enum":["__proto__",0],"type":"string"}},"items":{"$ref":"not-a-ref"},"required":["x"],"additionalProperties":true},"required":["a"],"additionalProperties":true}}}}},{"v":0,"s":{"enum":["",6],"type":"string"},"doc":{"components":{"schemas":{"S":{"anyOf":[{"enum":["__proto__",5],"type":"string"},{"allOf":[{"enum":["a b",4],"type":"string"},{"type":"string","format":"uuid"}]}]}}}}},{"v":true,"s":{"$ref":"#/components/schemas/S"},"doc":{"components":{"schemas":{"S":{"enum":["x&y=z",8],"type":"string"}}}}},{"v":{},"s":{"allOf":[{"type":"integer","properties":{"a":{"type":"string","properties":{"a":{"enum":["",4],"type":"string"}},"items":{"type":"object","properties":{"a":{"oneOf":[{"type":"string"},{"type":"string"}]}},"items":{"$ref":"#/nope"},"required":["a"],"additionalProperties":true},"required":[],"additionalProperties":true}},"items":{"type":"string","format":"date-time"},"required":["x"],"additionalProperties":false},{"type":"string","properties":{"a":{"type":"string","format":"uuid"}},"items":{"anyOf":[{"type":"string","format":"weird"},{"$ref":"not-a-ref"}]},"required":["x"],"additionalProperties":true}]},"doc":{"components":{"schemas":{"S":{"$ref":"not-a-ref"}}}}},{"v":{"a":0,"x":{"b":null}},"s":{"oneOf":[{"type":"integer","properties":{"a":{"$ref":"#/nope"}},"items":{"oneOf":[{"type":"string","format":"email"},{"anyOf":[{"enum":["a b",3],"type":"string"},{"type":"integer","properties":{"a":{}},"items":{},"required":["x"],"additionalProperties":false}]}]},"required":["x"],"additionalProperties":false},{"type":"boolean","properties":{"a":{"type":"array","properties":{"a":{"type":"string","properties":{"a":{"type":"array","properties":{"a":{"$ref":"#/components/schemas/S"}},"items":{"$ref":"#/components/schemas/S"},"required":[],"additionalProperties":true}},"items":{"$ref":"not-a-ref"},"required":["x"],"additionalProperties":true}},"items":{"oneOf":[{"type":"string","format":"date-time"},{"enum":["a",1],"type":"string"}]},"required":["a"],"additionalProperties":false}},"items":{"anyOf":[{"type":"string","format":"uri"},{"enum":["é\u0000",3],"type":"string"}]},"required":[],"additionalProperties":true}]},"doc":{"components":{"schemas":{"S":{"enum":["é\u0000",5],"type":"string"}}}}},{"v":"s","s":{"enum":["a",5],"type":"string"},"doc":{"components":{"schemas":{"S":{"type":"string","properties":{"a":{"oneOf":[{"enum":["é\u0000",2],"type":"string"},{"type":"string","format":"uri"}]}},"items":{"type":"object","properties":{"a":{"$ref":"not-a-ref"}},"items":{"allOf":[{"enum":["é\u0000",6],"type":"string"},{"type":"string","format":"uuid"}]},"required":["a"],"additionalProperties":true},"required":["a"],"additionalProperties":true}}}}},{"v":{"a":null},"s":{"$ref":"#/components/schemas/S"},"doc":{"components":{"schemas":{"S":{"enum":["a",3],"type":"string"}}}}},{"v":{"a":[{}]},"s":{"enum":["",0],"type":"string"},"doc":{"components":{"schemas":{"S":{"type":"string","properties":{"a":{"anyOf":[{"type":"boolean","properties":{"a":{"type":"string","format":"date-time"}},"items":{"allOf":[{"type":"string","format":"date-time"},{"enum":["a",3],"type":"string"}]},"required":["a"],"additionalProperties":false},{"$ref":"#/components/schemas/S"}]}},"items":{"type":"string","format":"email"},"required":["x"],"additionalProperties":true}}}}},{"v":-3,"s":{"enum":["{{v}}",1],"type":"string"},"doc":{"components":{"schemas":{"S":{"type":"integer","properties":{"a":{"enum":["",1],"type":"string"}},"items":{"anyOf":[{"enum":["é\u0000",4],"type":"string"},{"$ref":"not-a-ref"}]},"required":[],"additionalProperties":true}}}}},{"v":{"b":[]},"s":{"allOf":[{"enum":["../../etc",1],"type":"string"},{"oneOf":[{"type":"string","format":"uri"},{"type":"string","properties":{"a":{"type":"integer","properties":{"a":{"oneOf":[{"type":"string"},{}]}},"items":{"type":"boolean","properties":{"a":{}},"items":{},"required":[],"additionalProperties":false},"required":[],"additionalProperties":false}},"items":{"allOf":[{"type":"array","properties":{"a":{}},"items":{"type":"string"},"required":[],"additionalProperties":true},{"type":"boolean","properties":{"a":{"type":"string"}},"items":{"$ref":"#/components/schemas/S"},"required":[],"additionalProperties":false}]},"required":[],"additionalProperties":false}]}]},"doc":{"components":{"schemas":{"S":{"allOf":[{"type":"string","format":"email"},{"type":"string","format":"weird"}]}}}}},{"v":[[[[null],{}]]],"s":{"allOf":[{"$ref":"#/nope"},{"enum":["é\u0000",8],"type":"string"}]},"doc":{"components":{"schemas":{"S":{"$ref":"#/nope"}}}}},{"v":[null,0,{}],"s":{"enum":["x&y=z",7],"type":"string"},"doc":{"components":{"schemas":{"S":{"oneOf":[{"enum":["x&y=z",7],"type":"string"},{"$ref":"#/components/schemas/S"}]}}}}},{"v":[{"b":{"b":[{"b":{"a":null,"b":"s"}},"s",{"x":{"b":"s"},"b":null,"a":-3}]}},[]],"s":{"allOf":[{"oneOf":[{"type":"string","format":"weird"},{"$ref":"#/nope"}]},{"oneOf":[{"$ref":"#/nope"},{"oneOf":[{"type":"array","properties":{"a":{"allOf":[{"type":"string"},{"type":"string"}]}},"items":{"oneOf":[{"$ref":"#/components/schemas/S"},{"$ref":"#/components/schemas/S"}]},"required":[],"additionalProperties":true},{"$ref":"#/nope"}]}]}]},"doc":{"components":{"schemas":{"S":{"type":"integer","properties":{"a":{"oneOf":[{"$ref":"#/nope"},{"enum":["../../etc",1],"type":"string"}]}},"items":{"type":"boolean","properties":{"a":{"$ref":"not-a-ref"}},"items":{"type":"object","properties":{"a":{"$ref":"#/nope"}},"items":{"type":"string","format":"uri"},"required":[],"additionalProperties":true},"required":["a"],"additionalProperties":true},"required":["x"],"additionalProperties":false}}}}},{"v":[1.5,{"b":{"b":"s","a":1.5},"x":["../../etc"]}],"s":{"allOf":[{"enum":["{{v}}",6],"type":"string"},{"allOf":[{"$ref":"not-a-ref"},{"allOf":[{"$ref":"not-a-ref"},{"$ref":"#/components/schemas/S"}]}]}]},"doc":{"components":{"schemas":{"S":{"$ref":"#/components/schemas/S"}}}}},{"v":[false],"s":{"enum":["a",5],"type":"string"},"doc":{"components":{"schemas":{"S":{"oneOf":[{"type":"string","properties":{"a":{"type":"string","format":"weird"}},"items":{"$ref":"#/components/schemas/S"},"required":["x"],"additionalProperties":true},{"type":"object","properties":{"a":{"type":"integer","properties":{"a":{"type":"string","format":"uuid"}},"items":{"type":"array","properties":{"a":{"type":["string","null"],"properties":{"a":{"type":"string"}},"items":{"type":"string"},"required":[],"additionalProperties":false}},"items":{"allOf":[{"type":"string"},{"type":"string"}]},"required":[],"additionalProperties":false},"required":["x"],"additionalProperties":false}},"items":{"oneOf":[{"type":["string","null"],"properties":{"a":{"type":["string","null"],"properties":{"a":{"type":"string"}},"items":{"type":"string"},"required":["x"],"additionalProperties":true}},"items":{"enum":["a",4],"type":"string"},"required":["x"],"additionalProperties":true},{"type":"object","properties":{"a":{"allOf":[{"type":"string"},{"type":"string"}]}},"items":{"anyOf":[{"$ref":"#/components/schemas/S"},{"type":"string"}]},"required":[],"additionalProperties":false}]},"required":[],"additionalProperties":false}]}}}}},{"v":{},"s":{"type":"integer","properties":{"a":{"type":"boolean","properties":{"a":{"type":"string","format":"email"}},"items":{"anyOf":[{"allOf":[{"anyOf":[{"type":"string"},{"$ref":"#/components/schemas/S"}]},{"type":"string","format":"email"}]},{"type":"string","format":"uri"}]},"required":["x"],"additionalProperties":false}},"items":{"type":"string","format":"date-time"},"required":[],"additionalProperties":false},"doc":{"components":{"schemas":{"S":{"type":"object","properties":{"a":{"type":"array","properties":{"a":{"allOf":[{"$ref":"#/nope"},{"type":"boolean","properties":{"a":{"$ref":"#/nope"}},"items":{"type":"integer","properties":{"a":{}},"items":{"$ref":"#/components/schemas/S"},"required":["a"],"additionalProperties":true},"required":["x"],"additionalProperties":false}]}},"items":{"enum":["../../etc",7],"type":"string"},"required":["x"],"additionalProperties":true}},"items":{"oneOf":[{"type":"string","format":"email"},{"enum":["a",6],"type":"string"}]},"required":[],"additionalProperties":false}}}}},{"v":false,"s":{"enum":["../../etc",3],"type":"string"},"doc":{"components":{"schemas":{"S":{"type":"string","format":"uuid"}}}}},{"v":{"b":[{},-3]},"s":{"type":"object","properties":{"a":{"type":"string","format":"uuid"}},"items":{"type":"string","format":"email"},"required":[],"additionalProperties":false},"doc":{"components":{"schemas":{"S":{"$ref":"#/nope"}}}}},{"v":{"b":{}},"s":{"allOf":[{"$ref":"#/components/schemas/S"},{"enum":["é\u0000",6],"type":"string"}]},"doc":{"components":{"schemas":{"S":{"enum":["__proto__",0],"type":"string"}}}}},{"v":{},"s":{"enum":["aaaaaaaaaaaaaaaaaaa",5],"type":"string"},"doc":{"components":{"schemas":{"S":{"enum":["x&y=z",6],"type":"string"}}}}},{"v":{"x":{"x":1.5}},"s":{"enum":["",0],"type":"string"},"doc":{"components":{"schemas":{"S":{"type":"object","properties":{"a":{"enum":["",7],"type":"string"}},"items":{"type":"boolean","properties":{"a":{"$ref":"not-a-ref"}},"items":{"type":"string","format":"weird"},"required":["a"],"additionalProperties":false},"required":["x"],"additionalProperties":true}}}}},{"v":"a","s":{"enum":["a b",7],"type":"string"},"doc":{"components":{"schemas":{"S":{"type":"string","format":"date-time"}}}}},{"v":[0],"s":{"enum":["a",4],"type":"string"},"doc":{"components":{"schemas":{"S":{"oneOf":[{"type":"integer","properties":{"a":{"enum":["../../etc",0],"type":"string"}},"items":{"$ref":"not-a-ref"},"required":["x"],"additionalProperties":true},{"type":"array","properties":{"a":{"enum":["{{v}}",8],"type":"string"}},"items":{"oneOf":[{"allOf":[{"$ref":"not-a-ref"},{"type":"string","format":"date-time"}]},{"type":"string","format":"email"}]},"required":["a"],"additionalProperties":true}]}}}}},{"v":"../../etc","s":{"enum":["__proto__",7],"type":"string"},"doc":{"components":{"schemas":{"S":{"enum":["__proto__",5],"type":"string"}}}}},{"v":{},"s":{"allOf":[{"type":["string","null"],"properties":{"a":{"oneOf":[{"$ref":"not-a-ref"},{"enum":["é\u0000",6],"type":"string"}]}},"items":{"type":"string","format":"email"},"required":[],"additionalProperties":false},{"enum":["",3],"type":"string"}]},"doc":{"components":{"schemas":{"S":{"type":"string","format":"date-time"}}}}},{"v":{"a":{}},"s":{"type":"object","properties":{"a":{"type":"string","format":"uuid"}},"items":{"enum":["",0],"type":"string"},"required":["x"],"additionalProperties":true},"doc":{"components":{"schemas":{"S":{"enum":["x&y=z",7],"type":"string"}}}}},{"v":[{}],"s":{"enum":["a",6],"type":"string"},"doc":{"components":{"schemas":{"S":{"type":"array","properties":{"a":{"allOf":[{"oneOf":[{"$ref":"not-a-ref"},{"enum":["__proto__",1],"type":"string"}]},{"type":["string","null"],"properties":{"a":{"anyOf":[{"type":["string","null"],"properties":{"a":{}},"items":{"type":"string"},"required":["x"],"additionalProperties":true},{"type":["string","null"],"properties":{"a":{}},"items":{},"required":[],"additionalProperties":true}]}},"items":{"oneOf":[{"type":"boolean","properties":{"a":{"type":"string"}},"items":{"$ref":"#/components/schemas/S"},"required":[],"additionalProperties":false},{"enum":["x&y=z",6],"type":"string"}]},"required":["a"],"additionalProperties":false}]}},"items":{"enum":["é\u0000",7],"type":"string"},"required":["x"],"additionalProperties":false}}}}},{"v":{"a":"s","b":{}},"s":{"enum":["é\u0000",2],"type":"string"},"doc":{"components":{"schemas":{"S":{"allOf":[{"type":"integer","properties":{"a":{"type":"string","format":"uri"}},"items":{"type":"string","format":"email"},"required":[],"additionalProperties":true},{"anyOf":[{"anyOf":[{"anyOf":[{"anyOf":[{"type":"string"},{"type":"string"}]},{"type":"string","format":"weird"}]},{"type":"object","properties":{"a":{"oneOf":[{"type":"string"},{"$ref":"#/components/schemas/S"}]}},"items":{"enum":["a b",2],"type":"string"},"required":["x"],"additionalProperties":true}]},{"enum":["../../etc",3],"type":"string"}]}]}}}}},{"v":{"b":{"b":{"x":{"b":{"b":1.5}},"b":{"b":0}}}},"s":{"enum":["{{v}}",6],"type":"string"},"doc":{"components":{"schemas":{"S":{"type":["string","null"],"properties":{"a":{"type":"string","format":"weird"}},"items":{"type":"string","format":"uri"},"required":["a"],"additionalProperties":false}}}}},{"v":{"a":{},"x":{}},"s":{"allOf":[{"anyOf":[{"oneOf":[{"type":"string","format":"uri"},{"type":"object","properties":{"a":{"$ref":"not-a-ref"}},"items":{"type":"string","format":"date-time"},"required":[],"additionalProperties":false}]},{"enum":["../../etc",3],"type":"string"}]},{"allOf":[{"$ref":"#/components/schemas/S"},{"oneOf":[{"type":"integer","properties":{"a":{"type":"string","format":"uri"}},"items":{"type":"string","properties":{"a":{}},"items":{},"required":["a"],"additionalProperties":false},"required":["x"],"additionalProperties":false},{"type":"string","properties":{"a":{"$ref":"#/components/schemas/S"}},"items":{"allOf":[{"type":"string"},{}]},"required":[],"additionalProperties":true}]}]}]},"doc":{"components":{"schemas":{"S":{"type":"array","properties":{"a":{"type":"string","properties":{"a":{"type":"string","format":"uuid"}},"items":{"$ref":"#/components/schemas/S"},"required":["x"],"additionalProperties":true}},"items":{"type":"string","format":"weird"},"required":["a"],"additionalProperties":true}}}}},{"v":1.5,"s":{"allOf":[{"type":"object","properties":{"a":{"enum":["é\u0000",1],"type":"string"}},"items":{"enum":["é\u0000",7],"type":"string"},"required":["x"],"additionalProperties":false},{"anyOf":[{"type":"string","format":"weird"},{"type":"array","properties":{"a":{"type":"array","properties":{"a":{"oneOf":[{},{}]}},"items":{"type":"string","format":"weird"},"required":["x"],"additionalProperties":true}},"items":{"type":"string","format":"date-time"},"required":[],"additionalProperties":false}]}]},"doc":{"components":{"schemas":{"S":{"type":"string","properties":{"a":{"type":"object","properties":{"a":{"type":"string","format":"uri"}},"items":{"anyOf":[{"type":"string","format":"date-time"},{"$ref":"#/nope"}]},"required":["x"],"additionalProperties":true}},"items":{"anyOf":[{"allOf":[{"type":"string","format":"date-time"},{"$ref":"not-a-ref"}]},{"$ref":"#/nope"}]},"required":["x"],"additionalProperties":false}}}}},{"v":{"b":null},"s":{"enum":["__proto__",3],"type":"string"},"doc":{"components":{"schemas":{"S":{"anyOf":[{"type":"string","format":"date-time"},{"allOf":[{"enum":["__proto__",2],"type":"string"},{"enum":["{{v}}",6],"type":"string"}]}]}}}}},{"v":[1.5,-3,{"a":0}],"s":{"type":"array","properties":{"a":{"enum":["é\u0000",7],"type":"string"}},"items":{"enum":["x&y=z",3],"type":"string"},"required":["x"],"additionalProperties":true},"doc":{"components":{"schemas":{"S":{"$ref":"not-a-ref"}}}}},{"v":{"b":[],"x":{"b":["é\u0000",null],"x":{"b":{"b":null},"x":null}}},"s":{"allOf":[{"type":"object","properties":{"a":{"enum":["aaaaaaaaaaaaaaa",7],"type":"string"}},"items":{"anyOf":[{"type":"boolean","properties":{"a":{"oneOf":[{"type":"string"},{}]}},"items":{"$ref":"#/components/schemas/S"},"required":["a"],"additionalProperties":false},{"type":"array","properties":{"a":{"type":"string","format":"date-time"}},"items":{"$ref":"not-a-ref"},"required":["a"],"additionalProperties":false}]},"required":["a"],"additionalProperties":false},{"$ref":"not-a-ref"}]},"doc":{"components":{"schemas":{"S":{"$ref":"#/components/schemas/S"}}}}},{"v":{"b":1.5},"s":{"enum":["a b",0],"type":"string"},"doc":{"components":{"schemas":{"S":{"oneOf":[{"oneOf":[{"type":"string","format":"uri"},{"type":"boolean","properties":{"a":{"type":"string","properties":{"a":{"type":"string","format":"uri"}},"items":{"type":"string","format":"weird"},"required":[],"additionalProperties":true}},"items":{"oneOf":[{"enum":["{{v}}",4],"type":"string"},{"enum":["a b",3],"type":"string"}]},"required":[],"additionalProperties":false}]},{"type":"string","properties":{"a":{"$ref":"not-a-ref"}},"items":{"type":"array","properties":{"a":{"type":"string","format":"weird"}},"items":{"$ref":"not-a-ref"},"required":["x"],"additionalProperties":true},"required":[],"additionalProperties":false}]}}}}},{"v":false,"s":{"$ref":"#/components/schemas/S"},"doc":{"components":{"schemas":{"S":{"allOf":[{"enum":["x&y=z",6],"type":"string"},{"allOf":[{"anyOf":[{"type":"string","format":"uri"},{"type":"integer","properties":{"a":{"$ref":"not-a-ref"}},"items":{"enum":["../../etc",2],"type":"string"},"required":["a"],"additionalProperties":true}]},{"$ref":"not-a-ref"}]}]}}}}},{"v":0,"s":{"allOf":[{"enum":["",2],"type":"string"},{"type":"string","format":"uri"}]},"doc":{"components":{"schemas":{"S":{"type":"object","properties":{"a":{"$ref":"not-a-ref"}},"items":{"type":"string","format":"email"},"required":[],"additionalProperties":false}}}}},{"v":[{"x":[{"a":{}},false,{"b":["s"]}]},{"b":[{"b":[{"b":1,"x":null},true]}],"x":{"a":1.5}}],"s":{"allOf":[{"$ref":"#/components/schemas/S"},{"allOf":[{"anyOf":[{"$ref":"#/nope"},{"anyOf":[{"$ref":"not-a-ref"},{"type":"string","format":"weird"}]}]},{"type":"string","format":"email"}]}]},"doc":{"components":{"schemas":{"S":{"enum":["x&y=z",8],"type":"string"}}}}},{"v":{"b":[0,{"b":""}],"a":{}},"s":{"enum":["{{v}}",7],"type":"string"},"doc":{"components":{"schemas":{"S":{"$ref":"#/nope"}}}}},{"v":true,"s":{"allOf":[{"$ref":"#/components/schemas/S"},{"type":"string","format":"uuid"}]},"doc":{"components":{"schemas":{"S":{"enum":["__proto__",8],"type":"string"}}}}},{"v":{"b":-3,"x":{}},"s":{"enum":["",6],"type":"string"},"doc":{"components":{"schemas":{"S":{"$ref":"#/nope"}}}}},{"v":"é\u0000","s":{"$ref":"#/components/schemas/S"},"doc":{"components":{"schemas":{"S":{"enum":["a",6],"type":"string"}}}}},{"v":-3,"s":{"$ref":"#/components/schemas/S"},"doc":{"components":{"schemas":{"S":{"allOf":[{"type":"array","properties":{"a":{"type":"string","format":"weird"}},"items":{"type":"string","format":"email"},"required":["a"],"additionalProperties":false},{"$ref":"#/nope"}]}}}}},{"v":true,"s":{"allOf":[{"type":"string","format":"date-time"},{"allOf":[{"type":"string","properties":{"a":{"$ref":"#/components/schemas/S"}},"items":{"type":["string","null"],"properties":{"a":{"enum":["aaaaa",0],"type":"string"}},"items":{"anyOf":[{"$ref":"#/components/schemas/S"},{"$ref":"#/components/schemas/S"}]},"required":["x"],"additionalProperties":true},"required":["a"],"additionalProperties":true},{"$ref":"#/components/schemas/S"}]}]},"doc":{"components":{"schemas":{"S":{"enum":["",6],"type":"string"}}}}},{"v":[null],"s":{"allOf":[{"allOf":[{"allOf":[{"type":"string","format":"weird"},{"type":"string","format":"date-time"}]},{"type":"object","properties":{"a":{"$ref":"#/components/schemas/S"}},"items":{"type":["string","null"],"properties":{"a":{"type":"boolean","properties":{"a":{}},"items":{},"required":["a"],"additionalProperties":false}},"items":{"type":"string","format":"weird"},"required":["x"],"additionalProperties":false},"required":[],"additionalProperties":false}]},{"type":["string","null"],"properties":{"a":{"allOf":[{"type":"string","properties":{"a":{"type":"string","format":"uri"}},"items":{"type":"object","properties":{"a":{"type":"string"}},"items":{"$ref":"#/components/schemas/S"},"required":["x"],"additionalProperties":true},"required":["x"],"additionalProperties":true},{"enum":["é\u0000",4],"type":"string"}]}},"items":{"type":"integer","properties":{"a":{"enum":["aaaaaaaaaaaaaaaaaaaa",0],"type":"string"}},"items":{"anyOf":[{"type":"string","format":"email"},{"type":"string","format":"weird"}]},"required":["a"],"additionalProperties":false},"required":["x"],"additionalProperties":true}]},"doc":{"components":{"schemas":{"S":{"$ref":"not-a-ref"}}}}},{"v":{"x":null},"s":{"enum":["{{v}}",4],"type":"string"},"doc":{"components":{"schemas":{"S":{"type":["string","null"],"properties":{"a":{"type":"string","format":"date-time"}},"items":{"enum":["__proto__",0],"type":"string"},"required":["x"],"additionalProperties":true}}}}},{"v":false,"s":{"allOf":[{"anyOf":[{"enum":["a b",2],"type":"string"},{"enum":["",3],"type":"string"}]},{"type":"integer","properties":{"a":{"enum":["x&y=z",2],"type":"string"}},"items":{"type":"string","format":"uri"},"required":["x"],"additionalProperties":false}]},"doc":{"components":{"schemas":{"S":{"enum":["a b",8],"type":"string"}}}}},{"v":[1.5],"s":{"enum":["a",6],"type":"string"},"doc":{"components":{"schemas":{"S":{"type":"string","format":"email"}}}}},{"v":[{},0,[[],{},[{"x":[{},-3,{"a":null,"b":null}]},{}]]],"s":{"enum":["{{v}}",4],"type":"string"},"doc":{"components":{"schemas":{"S":{"enum":["a",8],"type":"string"}}}}},{"v":[0,"é\u0000"],"s":{"enum":["__proto__",0],"type":"string"},"doc":{"components":{"schemas":{"S":{"oneOf":[{"type":"string","format":"date-time"},{"$ref":"#/nope"}]}}}}},{"v":[null,{},{"x":{}}],"s":{"type":"array","properties":{"a":{"anyOf":[{"$ref":"not-a-ref"},{"anyOf":[{"$ref":"not-a-ref"},{"enum":["a",1],"type":"string"}]}]}},"items":{"allOf":[{"type":["string","null"],"properties":{"a":{"allOf":[{"$ref":"#/components/schemas/S"},{"$ref":"#/components/schemas/S"}]}},"items":{"allOf":[{"enum":["x&y=z",5],"type":"string"},{"$ref":"#/components/schemas/S"}]},"required":["a"],"additionalProperties":false},{"type":"boolean","properties":{"a":{"type":"array","properties":{"a":{"type":"boolean","properties":{"a":{"type":"string"}},"items":{"type":"string"},"required":["a"],"additionalProperties":false}},"items":{"type":"string","format":"uri"},"required":[],"additionalProperties":false}},"items":{"type":"integer","properties":{"a":{"type":"string","properties":{"a":{"type":"string"}},"items":{},"required":["a"],"additionalProperties":true}},"items":{"type":"object","properties":{"a":{"$ref":"#/components/schemas/S"}},"items":{},"required":["x"],"additionalProperties":false},"required":["x"],"additionalProperties":false},"required":["x"],"additionalProperties":false}]},"required":["a"],"additionalProperties":false},"doc":{"components":{"schemas":{"S":{"type":"string","format":"uri"}}}}}] \ No newline at end of file diff --git a/qa/fuzz/deep-fuzz.mjs b/qa/fuzz/deep-fuzz.mjs new file mode 100644 index 0000000..9aa5fca Binary files /dev/null and b/qa/fuzz/deep-fuzz.mjs differ diff --git a/qa/load/load-test.mjs b/qa/load/load-test.mjs new file mode 100644 index 0000000..428ebf4 --- /dev/null +++ b/qa/load/load-test.mjs @@ -0,0 +1,123 @@ +#!/usr/bin/env node +// Throughput / latency / leak SLO harness for the local servers (item 3 of Issue #14). +// +// Defines a conservative SLO for the local-first servers and gates against it: +// • correctness under load — error rate MUST be 0 (every request gets a valid response) +// • no memory leak — post-GC RSS growth over a sustained soak MUST stay under --rss-cap-mb +// • tail latency — p99 under --p99-cap-ms (generous; CI runners vary) +// • throughput — reported, with a low floor (--rps-floor) any runner clears +// +// node --expose-gc qa/load/load-test.mjs [--seconds S] [--concurrency C] [--rss-cap-mb N] +// [--p99-cap-ms N] [--rps-floor N] [--target mock|web] +// +// Requires a built @truspec/core (+ @truspec/web for --target web). Run `pnpm build` first. +import http from "node:http"; +import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { startMockServer } from "../../packages/core/dist/mock/index.js"; + +const HERE = dirname(fileURLToPath(import.meta.url)); + +const argv = process.argv.slice(2); +const flag = (n, d) => { const i = argv.indexOf(n); return i >= 0 ? Number(argv[i + 1]) : d; }; +const sflag = (n, d) => { const i = argv.indexOf(n); return i >= 0 ? argv[i + 1] : d; }; +const SECONDS = flag("--seconds", 20); +const CONC = flag("--concurrency", 64); +const RSS_CAP = flag("--heap-cap-mb", flag("--rss-cap-mb", 15)); +// Gate on p95, not p99: a few transient GC/scheduling spikes blow up p99 on a shared runner but barely +// move p95, so p95 catches a real latency regression (a consistent slowdown) without flaking on jitter. +const P95_CAP = flag("--p95-cap-ms", flag("--p99-cap-ms", 500)); +const RPS_FLOOR = flag("--rps-floor", 200); +const TARGET = sflag("--target", "mock"); + +const SPEC = `openapi: 3.0.3 +info: { title: Load, version: "1" } +paths: + /pets/{id}: + get: + responses: + "200": { content: { application/json: { schema: { type: object, properties: { id: { type: integer }, name: { type: string } } } } } } +`; + +const hist = new Int32Array(5001); // ms buckets; last = overflow +const record = (ms) => { hist[Math.min(5000, Math.round(ms))]++; }; +const pct = (p) => { const total = hist.reduce((a, b) => a + b, 0); let target = total * p, cum = 0; for (let i = 0; i < hist.length; i++) { cum += hist[i]; if (cum >= target) return i; } return 5000; }; +const gc = () => { if (global.gc) { global.gc(); global.gc(); } }; +const mb = (b) => Math.round((b / 1048576) * 10) / 10; +const rssMB = () => mb(process.memoryUsage().rss); +// Leak signal = retained heap AFTER a forced GC. RSS is a poor leak metric: V8 keeps the high-water +// mark and rarely returns freed pages to the OS, so RSS climbs under load even with no leak. A real +// leak shows up as growing post-GC heapUsed (objects that stay reachable); steady state stays flat. +const heapMB = () => mb(process.memoryUsage().heapUsed); + +async function startServer() { + if (TARGET === "mock") { + const h = await startMockServer(SPEC, { port: 0 }); + return { url: h.url, path: "/pets/1", close: () => h.close() }; + } + const { startWebServer } = await import("../../packages/web/dist/server/index.js"); + const dir = mkdtempSync(join(tmpdir(), "load-")); + mkdirSync(join(dir, "environments"), { recursive: true }); + writeFileSync(join(dir, "environments", "local.env.yaml"), 'tspec: "0.1"\nname: local\nvariables: {}\n'); + writeFileSync(join(dir, "g.tspec.yaml"), 'tspec: "0.1"\nname: G\nurl: "http://x"\nassertions: []\n'); + // Load the static client route (GET /) — a fair test of the server's raw HTTP throughput + leak + // behaviour. /api/state deliberately re-scans the workspace per call (not a hot path), so it isn't a + // representative throughput target. clientDir points at the built SPA. + const clientDir = resolve(HERE, "../../packages/web/dist/client"); + const h = await startWebServer({ dir, port: 0, clientDir }); + return { url: h.url, path: "/", close: async () => { await h.close(); rmSync(dir, { recursive: true, force: true }); } }; +} + +function hammer(agent, base, path, deadline, stats) { + return new Promise((done) => { + const fire = () => { + if (Date.now() >= deadline) return done(); + const t0 = Date.now(); + const req = http.get(`${base}${path}`, { agent }, (res) => { + res.resume(); + res.on("end", () => { record(Date.now() - t0); stats.ok += res.statusCode >= 200 && res.statusCode < 500 ? 1 : 0; stats.err += res.statusCode >= 500 ? 1 : 0; fire(); }); + }); + req.on("error", () => { stats.err++; fire(); }); + }; + fire(); + }); +} + +const srv = await startServer(); +const agent = new http.Agent({ keepAlive: true, maxSockets: CONC }); +const stats = { ok: 0, err: 0 }; +try { + // warm up (not measured) + await new Promise((r) => { const a = new http.Agent({ keepAlive: true }); let n = 0; const f = () => http.get(`${srv.url}${srv.path}`, { agent: a }, (res) => { res.resume(); res.on("end", () => (++n < 200 ? f() : r())); }).on("error", () => r()); f(); }); + gc(); const heapBefore = heapMB(), rssBefore = rssMB(); + + const start = Date.now(); + const deadline = start + SECONDS * 1000; + await Promise.all(Array.from({ length: CONC }, () => hammer(agent, srv.url, srv.path, deadline, stats))); + const elapsed = (Date.now() - start) / 1000; + + gc(); const heapAfter = heapMB(), rssAfter = rssMB(); + const total = stats.ok + stats.err; + const rps = Math.round(total / elapsed); + const heapGrowth = Math.round((heapAfter - heapBefore) * 10) / 10; + const p50 = pct(0.5), p95 = pct(0.95), p99 = pct(0.99); + + console.log(`target=${TARGET} conc=${CONC} dur=${elapsed.toFixed(1)}s`); + console.log(`requests=${total} ok=${stats.ok} err=${stats.err} rps=${rps}`); + console.log(`latency p50=${p50}ms p95=${p95}ms p99=${p99}ms`); + console.log(`heapUsed(post-GC) ${heapBefore}MB -> ${heapAfter}MB (growth ${heapGrowth}MB over ${total} reqs) [rss ${rssBefore}->${rssAfter}MB info]`); + + const fails = []; + if (stats.err !== 0) fails.push(`error rate: ${stats.err} failed requests (SLO 0)`); + if (heapGrowth > RSS_CAP) fails.push(`memory: post-GC heap grew ${heapGrowth}MB > ${RSS_CAP}MB cap (leak)`); + if (p95 > P95_CAP) fails.push(`latency: p95 ${p95}ms > ${P95_CAP}ms cap`); + if (rps < RPS_FLOOR) fails.push(`throughput: ${rps} rps < ${RPS_FLOOR} floor`); + + if (fails.length) { console.error("\nSLO VIOLATIONS:"); for (const f of fails) console.error(" ✗ " + f); process.exitCode = 1; } + else console.log("\n✓ all SLOs met"); +} finally { + await srv.close(); + agent.destroy(); +} diff --git a/stryker.config.json b/stryker.config.json index a75a23a..8cc6120 100644 --- a/stryker.config.json +++ b/stryker.config.json @@ -4,7 +4,7 @@ "packageManager": "pnpm", "testRunner": "vitest", "plugins": ["@stryker-mutator/vitest-runner"], - "vitest": { "configFile": "vitest.config.ts" }, + "vitest": { "configFile": "vitest.mutation.config.ts" }, "coverageAnalysis": "perTest", "reporters": ["clear-text", "json"], "jsonReporter": { "fileName": "qa/mutation-report.json" }, @@ -13,8 +13,13 @@ "timeoutMS": 20000, "ignoreStatic": true, "thresholds": { "high": 90, "low": 80, "break": 80 }, - "_mutateNote": "Scoped to the most security-critical module (the contract validator) for an inline run; measured 85.4%. A scheduled CI job should expand `mutate` to resolve.ts, mock/engine.ts, runner/run.ts, importers/* (796+ mutants for 3 modules alone — too slow inline).", + "_mutateNote": "Scoped to the highest-risk pure-logic modules. The weekly mutation.yml job can widen this to mock/engine.ts, runner/run.ts, and importers/* as the time budget allows.", "mutate": [ - "packages/core/src/spec/validate-response.ts" + "packages/core/src/spec/validate-response.ts", + "packages/core/src/runner/resolve.ts", + "packages/core/src/runner/capture.ts", + "packages/core/src/spec/scaffold.ts", + "packages/core/src/spec/drift.ts", + "packages/core/src/mock/engine.ts" ] } diff --git a/vitest.mutation.config.ts b/vitest.mutation.config.ts new file mode 100644 index 0000000..e3b08f7 --- /dev/null +++ b/vitest.mutation.config.ts @@ -0,0 +1,13 @@ +import { mergeConfig } from "vitest/config"; +import base from "./vitest.config"; + +// Config used ONLY by Stryker (stryker.config.json -> vitest.configFile). Mutation testing re-runs the +// covering tests for every mutant, so a slow smoke test in the covering set multiplies the whole run. +// fuzz.test.ts (~11s, property fuzz) covers many engine/runner/spec mutants and made Stryker crawl; +// exclude it here — the deterministic UNIT tests are what should kill mutants. The bounded fuzz still +// runs in the normal `pnpm test` and per-PR CI. +export default mergeConfig(base, { + test: { + exclude: ["**/node_modules/**", "**/dist/**", "**/fuzz.test.ts"], + }, +});