7.8 KiB
ADDED Requirements
Requirement: Package layout and dependency policy
The @suaveplan/testing-load package SHALL live at packages/testing/testing-load/. Its dependencies, peerDependencies, and devDependencies MUST NOT list any @suaveplan/* workspace package other than @suaveplan/types. External deps SHALL be referenced via catalog entries (no bare version strings). The package SHALL declare "sideEffects": false and SHALL provide a subpath export per feature module per openspec/AGENTS.md §6.
Scenario: Workspace deps are limited to @suaveplan/types
- WHEN
bunx turbo ls --filter=@suaveplan/testing-load...is run - THEN the only
@suaveplan/*package in the forward closure MUST be@suaveplan/types
Scenario: No bare versions
- WHEN the package's
package.jsondependenciesanddevDependenciesare scanned - THEN every external dep MUST resolve to a
catalog:<key>reference AND no bare semver string MUST appear
Requirement: Public API — defineLoadScenario, runLoadScenario, assertThresholds
The package SHALL export defineLoadScenario(spec) returning a frozen LoadScenario, runLoadScenario(scenario, opts?) returning Promise<{ summary: LoadSummary; raw: unknown }>, and assertThresholds(summary) returning void (throws on violation). Inputs SHALL be Zod-validated at the boundary; outputs SHALL conform to the LoadSummary shape regardless of which driver produced them. Every public function SHALL accept an optional { signal?: AbortSignal } and SHALL respect cancellation at bounded checkpoints.
Scenario: defineLoadScenario validates input
- WHEN
defineLoadScenario({ name: "x", target: "not-a-url", vus: -1, duration: "30s" })is called - THEN it MUST throw
TestingLoadErrorwithcode === "TESTING_LOAD.CONFIG_INVALID"ANDcontext.issuesMUST list the offending fields
Scenario: runLoadScenario honours AbortSignal
- WHEN
runLoadScenario(scenario, { signal })is awaited andsignal.abort()is called mid-run - THEN the promise MUST reject with
TestingLoadErrorwherecode === "TESTING_LOAD.ABORTED"ANDcause === signal.reason
Scenario: assertThresholds enumerates failed predicates
- WHEN a
LoadSummaryreportshttp_req_duration{p(95)} = 750against thresholdp(95)<500 - THEN
assertThresholds(summary)MUST throwTestingLoadErrorwithcode === "TESTING_LOAD.THRESHOLD_VIOLATION"ANDcontext.failedMUST list the predicate"http_req_duration: p(95)<500 (observed 750)"
Requirement: Driver pluggability — k6 primary, autocannon fallback
The package SHALL expose a LoadDriver interface and SHALL ship two implementations: a k6 driver invoking grafana/k6 via testcontainers, and an autocannon driver running in-process via the autocannon npm package. runLoadScenario SHALL default to the k6 driver when reachable and SHALL fall back to autocannon when k6 is unavailable. Both drivers SHALL normalise their output into the same LoadSummary shape so callers and assertThresholds are driver-agnostic.
Scenario: k6 selected when available
- WHEN
runLoadScenario(scenario)is called without an explicit driver AND a reachable Docker daemon is detected AND thegrafana/k6image can be pulled - THEN the k6 driver MUST be selected AND the resulting
summary.driverNameMUST equal"k6"
Scenario: autocannon fallback when k6 unavailable
- WHEN
runLoadScenario(scenario)is called without an explicit driver AND no Docker daemon is reachable - THEN the autocannon driver MUST be selected AND the resulting
summary.driverNameMUST equal"autocannon"
Scenario: Both drivers produce LoadSummary
- WHEN the same
LoadScenariois executed once via the k6 driver and once via the autocannon driver - THEN both runs MUST return a
LoadSummarywith the same set of metric keys (e.g.http_req_duration,http_req_failed,iteration_duration) ANDassertThresholdsMUST evaluate identically against either summary
Requirement: Default thresholds and per-scenario overrides
The package SHALL apply default thresholds when the caller omits the thresholds field on a LoadScenario: http_req_duration{p(95)} < 500ms, http_req_failed < 1% (rate < 0.01), iteration_duration{p(99)} < 2000ms. Caller-supplied thresholds SHALL merge on top of (not replace) the defaults; an explicit thresholds: {} SHALL retain the defaults; callers SHALL be able to override a single key without restating the rest.
Scenario: Defaults applied when omitted
- WHEN
defineLoadScenario({ name: "x", target: "http://t/", vus: 1, duration: "30s" })is called and the resulting scenario is inspected - THEN the resolved
thresholdsMUST containhttp_req_duration: ["p(95)<500"]ANDhttp_req_failed: ["rate<0.01"]ANDiteration_duration: ["p(99)<2000"]
Scenario: Single-key override merges with defaults
- WHEN
defineLoadScenario({ ..., thresholds: { http_req_duration: ["p(95)<200"] } })is called - THEN the resolved scenario's
http_req_durationMUST equal["p(95)<200"]AND the other default keys MUST remain present unchanged
Requirement: Run-mode presets — smoke, load, stress, spike
The package SHALL export named run-mode presets composable with defineLoadScenario: smoke (1 vu, 30s), load (10 vu, 5m), stress (50 vu, 10m), spike (stages from 0 to 100 vu over 30s). Presets SHALL be plain objects spreadable into defineLoadScenario's spec argument; callers SHALL be able to override any preset field on a per-scenario basis.
Scenario: smoke preset values
- WHEN the
smokepreset is inspected - THEN
vus === 1ANDduration === "30s"ANDstagesMUST be undefined
Scenario: spike preset values
- WHEN the
spikepreset is inspected - THEN
stagesMUST describe a ramp from0to100virtual users over30sAND the total scenario duration MUST equal the sum of stage durations
Requirement: Structured errors with TESTING_LOAD.* codes
The package SHALL throw only subclasses of TestingLoadError extends Error with stable codes prefixed TESTING_LOAD.*. Errors MUST carry { code, cause?, context? } fields. The package MUST NOT throw plain Error or TypeError from any public code path. Defined codes include TESTING_LOAD.CONFIG_INVALID, TESTING_LOAD.DRIVER_FAILED, TESTING_LOAD.DRIVER_UNAVAILABLE, TESTING_LOAD.THRESHOLD_VIOLATION, and TESTING_LOAD.ABORTED.
Scenario: Driver failure produces coded error
- WHEN the k6 driver's container exits non-zero
- THEN the driver MUST throw
TestingLoadErrorwithcode === "TESTING_LOAD.DRIVER_FAILED"ANDcontext.driverName === "k6"ANDcontext.exitCodeMUST equal the observed exit code
Scenario: Driver unavailable surfaces a typed error
- WHEN
runLoadScenariois called withdriver: "k6"explicitly AND no Docker daemon is reachable - THEN the call MUST throw
TestingLoadErrorwithcode === "TESTING_LOAD.DRIVER_UNAVAILABLE"ANDcontext.driverName === "k6"
Requirement: 100% Istanbul coverage and self-testability
The package SHALL meet 100% Istanbul statement / branch / function / line coverage. The k6 driver SHALL accept injectable exec and fetch shims so the package's own test suite runs without a Docker daemon. The autocannon driver SHALL accept an injectable autocannon factory so the package's own test suite runs without performing real network requests.
Scenario: Coverage gate at 100%
- WHEN
bunx turbo test:coverage --filter=@suaveplan/testing-loadruns - THEN statement, branch, function, and line coverage MUST each be 100%
Scenario: Tests pass without Docker
- WHEN the package's test suite is invoked on a host with no Docker daemon and no network access
- THEN every test MUST pass AND no test MUST attempt a real
dockerinvocation or a real outbound network request