Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,46 @@ jobs:

- name: Verify package contents
run: npm pack --dry-run

load-e2e:
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v6
with:
# Keep the load evidence bound to the reviewed head, matching the
# package job's head-bound attestation.
ref: ${{ github.event.pull_request.head.sha || github.sha }}

- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: 20
cache: npm

- name: Install dependencies
run: npm ci

- name: Install kind
run: |
curl --fail --location --output kind https://kind.sigs.k8s.io/dl/v0.31.0/kind-linux-amd64
echo 'eb244cbafcc157dff60cf68693c14c9a75c4e6e6fedaf9cd71c58117cb93e3fa kind' | sha256sum --check
chmod +x kind
sudo mv kind /usr/local/bin/kind

- name: Run in-cluster load harness E2E
run: |
trap 'kind delete cluster --name factory-load-e2e' EXIT
kind create cluster --name factory-load-e2e --image kindest/node:v1.35.0@sha256:452d707d4862f52530247495d180205e029056831160e22870e37e3f6c1ac31f
npm run test:e2e:load

- name: Upload load evidence
if: always()
uses: actions/upload-artifact@v6
with:
name: factory-load-evidence-${{ github.event.pull_request.head.sha || github.sha }}
path: artifacts/load-e2e/*.json
if-no-files-found: error
70 changes: 70 additions & 0 deletions docs/load-harness.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# In-cluster load harness

`runLoad(environment, profile)` creates a k6 Job in the environment's Kubernetes
namespace, waits for it to complete, captures its summary, and returns a
structured SLO verdict. The environment endpoint map should contain URLs that
are reachable from a pod; Kubernetes Service DNS names are preferred when the
target stack runs in the same cluster.

```ts
import { runLoad } from '@agent-relay/factory'

const result = await runLoad(
{
id: 'verify-123',
namespace: 'verify-123',
endpoints: { api: 'http://api.verify-123.svc.cluster.local:8080' },
},
{
name: 'api-load',
targets: [
{ name: 'read', endpoint: 'api', path: '/items', weight: 4 },
{
name: 'write',
endpoint: 'api',
path: '/items',
method: 'POST',
body: { value: 'example' },
},
],
vus: 50,
duration: '30s',
ramp: { up: '5s', down: '2s' },
thresholds: {
maxP95LatencyMs: 2_000,
maxP99LatencyMs: 5_000,
maxErrorRate: 0.01,
minThroughputRps: 10,
},
},
{ evidencePath: 'artifacts/load.json' },
)

if (!result.passed) {
console.error(result.violations)
}
```

Set `rps` to use an arrival-rate scenario; `vus` then controls preallocated
workers and `maxVus` caps scaling. Without `rps`, k6 runs a VU-based scenario.
Profiles can also be checked in as JSON or YAML and loaded with
`loadLoadProfile(path)`.

The returned `evidence` and `evidenceJson` include request count, p95 and p99,
error rate, throughput, a cumulative latency histogram, thresholds, and every
violated metric. Request headers and bodies are deliberately omitted from
evidence. The ephemeral ConfigMap and Job are deleted by default; pass
`cleanup: false` when a surrounding environment teardown owns cleanup.
The gate also fails closed with a `requestCount` violation when k6 completes
without sending any requests, even if the profile omitted a throughput floor.

Run the real kind-cluster proof with:

```bash
kind create cluster --name factory-load-e2e
npm run test:e2e:load
kind delete cluster --name factory-load-e2e
```

The E2E deploys a healthy service, drives a 50-VU load by default, records its
evidence, asserts a lenient SLO passes, and asserts a strict p95 SLO fails.
35 changes: 35 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
"build": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json",
"featuremap:check": "node bin/factory.mjs featuremap check",
"test": "vitest run",
"test:e2e:load": "tsx test/e2e/load-harness.e2e.ts",
"verify:e2e": "node scripts/verify-packed-e2e.mjs",
"factory-build": "factory-build/run-factory-build.sh"
},
Expand Down Expand Up @@ -93,6 +94,7 @@
"@types/proper-lockfile": "^4.1.4",
"esbuild": "^0.28.1",
"tsc-alias": "^1.8.17",
"tsx": "4.23.1",
"typescript": "^5.7.0",
"vitest": "^4.1.8"
}
Expand Down
Loading