# Business rules — test_harness

Evidence date: 2026-10-01.

Source: `git@bitbucket.org:overdrive/test_harness.git`, `main` at `9f2f006`.

No business-rules document existed in the source repository or in Silicon Ark. Rules below are taken from code and the two READMEs. Each rule is marked **Documented**, **Implemented**, or **Inferred**. Conflicts are left as conflicts.

This repository is a test harness. The rules are about how the harness behaves, not about a golf product's production policy. The coordinate list is fixture data.

## Access

### BR-001 — Express routes are unauthenticated

**Implemented.** `GET /health` and `POST /trigger-test` have no auth middleware, API key, or IP check.

**Documented** recommendation, not a rule that the code enforces: `README.md` says "API endpoints should be protected in production".

Failure mode of the missing protection: any caller who can open the port can start k6.

Evidence: `src/server.js`, `README.md` Security section.

### BR-002 — CORS allows the default `cors` package behaviour

**Implemented.** `app.use(cors())` is called with no options.

**Inferred:** the `cors` package default reflects the request origin and allows it. This inspection did not execute the server.

Evidence: `src/server.js`.

### BR-003 — Lambda handler has no auth check

**Implemented.** `exports.handler` does not read a token, signature, or IAM context. If an authorizer exists, it is outside this repository.

Evidence: `lambda-sqs-handler/index.js`.

### BR-004 — SSM document targets one instance tag

**Implemented** in the template. Every `aws:runCommand` step targets tag key `Name`, value `tagmarshal-test-harness-test-instance`.

**Unknown / Requires Confirmation:** that tag is actually applied, and that this document has been created in an account.

Evidence: `scripts/TagmarshalTestHarnessSSMDocument V1.yaml`.

## Trigger and URL rules

### BR-005 — A trigger requires `lambdaUrl`

**Implemented** and **Documented.** `POST /trigger-test` without `lambdaUrl` returns 400 and does not start k6. The README sample body uses that field name.

Evidence: `src/server.js`, `README.md`.

### BR-006 — The trigger always runs one script

**Implemented.** The command is `k6 run src/k6-script.js -e LAMBDA_URL=` plus the body value. No other script can be selected through this API.

Evidence: `src/server.js`.

### BR-007 — k6 URL falls back to a hard-coded host

**Implemented.**

| Script | Fallback host |
|---|---|
| `src/k6-script.js` | `https://tfl2idmvci.execute-api.us-east-2.amazonaws.com/Stage/device/fix/863717050222574` |
| `src/k6-script-updated.js` | `https://lt19abpplb.execute-api.us-east-2.amazonaws.com/Stage/device/fix/863717050222574` |
| `src/K6-Updated-MemEfficientS3Archive.js` | same `lt19abpplb` URL |

The path segment `863717050222574` is the same in all three. **Unknown / Requires Confirmation:** what that segment identifies, and which host is current.

Evidence: the three files.

### BR-008 — `npm test` uses the fallback URL

**Implemented.** `package.json` `test` is `k6 run src/k6-script.js` with no `-e LAMBDA_URL`. The pipeline runs that command. Combined with BR-007, a pipeline agent that has k6 will POST to the `tfl2idmvci` URL.

Evidence: `package.json`, `bitbucket-pipelines.yml`, `src/k6-script.js`.

## Load shape

### BR-009 — Active profile for the server script is 600 VUs for 7.2 seconds

**Implemented** in the `options` export of `src/k6-script.js` (`vus` and `duration`).

**Documented** conflict: `README.md` describes a five-stage ramp from 0 to 1000 users and back, lasting 15 minutes. That block is commented out in the script.

Evidence: `src/k6-script.js`, `README.md`.

### BR-010 — Updated script defaults

**Implemented** in `src/k6-script-updated.js`.

- Batch size 227 if `BATCH_SIZE` is unset.
- Wait 250 ms if `WAIT_TIME_MS` is unset.
- Duration 1 minute if the three duration variables are unset (0 days, 0 hours, 1 minute).
- Arrival executor `constant-arrival-rate`, `maxVUs` = batch × 2, response bodies discarded.

Evidence: `src/k6-script-updated.js`.

### BR-011 — Memory-oriented script defaults

**Implemented** in `src/K6-Updated-MemEfficientS3Archive.js`.

- Batch size 121, wait 400 ms, duration 7 days, unless env vars override them.
- Same executor shape as BR-010.
- Checks run when `__ITER % 10 === 0`, not every iteration.

The README example passes `121` and `400` into the shell, which forwards them as env vars, and passes a duration string instead of relying on the 7-day default.

Evidence: `src/K6-Updated-MemEfficientS3Archive.js`, `README.md`.

### BR-012 — A check failure does not stop the test

**Implemented.** `check(...)` records pass/fail. No `thresholds` object is set in any of the three scripts, so k6 is not told to fail the process on a missed check.

Evidence: `src/k6-script.js`, `src/k6-script-updated.js`, `src/K6-Updated-MemEfficientS3Archive.js`.

### BR-013 — Success check is status 200 and under 500 ms

**Implemented** in all three scripts. The memory-oriented script also sets `error_rate` to 1 when status is not 200, including the synthetic status 0 it uses if `http.post` throws.

Evidence: the three scripts.

## Payload rules

### BR-014 — k6 sends one fix object per request

**Implemented.** The body is a single object. It is not wrapped in `{ "fixes": [...] }`. `source` is the string `Test`. `Content-Type` is `application/json`.

`src/k6-script.js` and `src/k6-script-updated.js` pass `JSON.stringify(...)`. The memory-oriented script passes the object value itself.

Evidence: the three scripts.

### BR-015 — Field sources differ by script

**Implemented.**

| Field | `k6-script.js` | `k6-script-updated.js` and memory-oriented script |
|---|---|---|
| `accuracy` | random integer 0–10 | random integer 0–10 |
| `speed` | random integer 0–30 | random integer 0–30 |
| `battery` | random integer 0–100 | random integer 0–100 |
| `lat`, `lng` | from the course row selected by a shared index | from the selected course row |
| `hole` | random integer 1–18, then stringified | the row's `hole`, stringified, or `null` for Halfway |
| `number` | random integer 1–10 | the row's `number` |
| `goalTime` | random integer 1–1000 | the row's `goalTime` |
| `section` | not sent (commented out) | not sent |
| `date` | `new Date().toISOString()` every payload | every payload in the updated script; every 1000 updates in the memory-oriented script |
| `name` | not sent | not sent |

Evidence: the three scripts.

### BR-016 — Course list shape

**Implemented** fixture, not a written policy.

- 53 rows, duplicated in three files (two inline, one module).
- Holes are numbered 1 through 18 in the `hole` field.
- `number` runs through the 50s and skips 29. Row `number` 28 is `Halfway` with `hole: null` and `goalTime` 1200.
- Names are strings such as `Hole 1 Tee`, `Hole 1 Fairway`, `Hole 1 Green`.

**Unknown / Requires Confirmation:** which course this list belongs to, and whether skipping `number` 29 is intentional.

Evidence: `src/fixData.js` and the two inline copies.

### BR-017 — Lambda fix validity

**Implemented** and **Documented** (sample JSON in `lambda-sqs-handler/README.md`).

A fix must be an object with:

- numbers: `accuracy`, `speed`, `battery`, `number`, `lat`, `lng`
- strings: `source`, `hole`, `section`, `date`

If `fixes` is not an array, the handler returns 400 and does not call SQS. If any element fails, it returns 400 with `invalidCount` and does not send the valid ones. An empty array skips SQS and returns 200 with `count` 0.

This rule rejects the k6 body from BR-014 and BR-015: wrong envelope, `section` absent, and `hole` may be null.

Evidence: `lambda-sqs-handler/index.js`.

### BR-018 — SQS message envelope

**Implemented.** Each queue body is `{ fix, metadata: { timestamp: <ISO now>, source: "lambda-handler" } }`. More than one fix uses `SendMessageBatch` in groups of 10. One fix uses `SendMessage`. The handler does not read SQS batch failure entries before returning 200.

Evidence: `lambda-sqs-handler/index.js`. The README says data is written to the queue and does not describe the metadata wrapper.

### BR-019 — Lambda error body includes the exception message

**Implemented.** The 500 JSON has `error` equal to `Internal server error` and `message` equal to `error.message`.

Evidence: `lambda-sqs-handler/index.js`.

## Deploy and file rules

### BR-020 — Only `main` creates a CodeDeploy deployment

**Implemented.** `pipelines.default` and other branches stop after build-and-test. `pipelines.branches.main` adds the S3 and CodeDeploy step.

Application name and deployment group are the literals `lambda-load-test` and `lambda-load-test-group`. The zip key is `lambda-load-test/deployment.zip`. Overwrite is requested.

Evidence: `bitbucket-pipelines.yml`.

### BR-021 — CodeDeploy install path and process manager

**Implemented.**

- Files land in `/opt/lambda-load-test`.
- Hooks run as root.
- The Node process is PM2 name `lambda-load-test`, user `ec2-user` for the startup helper.
- `.env` is written with `PORT=3000`, `NODE_ENV=production`, and whatever `LAMBDA_URL` is in that shell.
- Validation waits 10 seconds and requires the health JSON fragment.

Evidence: `appspec.yml`, `scripts/after_install.sh`, `scripts/start_application.sh`, `scripts/validate_service.sh`.

### BR-022 — Shell runner files and bucket

**Implemented** literals:

- Bucket `k6testharnessrepo`.
- Host directory `/home/ssm-user/test_harness`.
- Scripts and logs under `src/newversion/` (that directory is not in git).
- CSV header `Timestamp,ResponseTime(ms),ErrorRate`.
- Archive when the CSV is at least 200 MB, and again at the end of the wrapper.
- Processes are staggered by 1 second. The starter polls every 10 seconds.

**Inferred** from the wrapper: the duration used for the loop is read from `total_seconds`, which is never set (`totalSeconds` is set instead), so the committed wrapper does not enter its k6 loop.

Evidence: `src/start_k6_processes.sh`, `src/run_k6_wrapper.sh`, `README.md`.

### BR-023 — SSM install does not start the test

**Implemented** in the document. The last step deletes `/tmp/test_harness` and the zip. No step runs `start_k6_processes.sh`.

Evidence: `scripts/TagmarshalTestHarnessSSMDocument V1.yaml`. The README's "Run the Test Harness" block is a manual follow-up, not part of the document.

## Conflicts (not resolved)

| Topic | One side | Other side |
|---|---|---|
| Load shape | README ramp to 1000 users | `k6-script.js` 600 VUs for 7.2 seconds |
| Payload | k6 single object, no `section` | Lambda `{ fixes: [...] }` with string `section` |
| Install root | `/opt/lambda-load-test`, `ec2-user`, yum | `/home/ssm-user/test_harness`, apt, `src/newversion` |
| Artifact | `s3://$AWS_S3_BUCKET/lambda-load-test/deployment.zip` | `s3://k6testharnessrepo/test_harness.zip` |
| Default API host | `tfl2idmvci` in the script the server runs | `lt19abpplb` in the later scripts |
| IAM template purpose | Description: MongoDB config servers | Role name and output: Test Harness |

**Unknown / Requires Confirmation** for each row: which side operators treat as the real rule.
