@osfactory/har 1.15.0 → 1.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +58 -7
- package/dist/templates/plugins/cypress/.github/workflows/cypress.yml +25 -0
- package/dist/templates/plugins/cypress/.har/stages/CYPRESS.md +81 -0
- package/dist/templates/plugins/cypress/.har/stages/cypress-e2e.sh +134 -0
- package/dist/templates/plugins/cypress/README.md +14 -0
- package/dist/templates/plugins/cypress/cypress/e2e/api.cy.js +13 -0
- package/dist/templates/plugins/cypress/cypress/e2e/smoke.cy.js +7 -0
- package/dist/templates/plugins/cypress/cypress/support/e2e.js +1 -0
- package/dist/templates/plugins/cypress/cypress.config.js +68 -0
- package/dist/templates/plugins/cypress/package.fragment.json +10 -0
- package/dist/templates/plugins/cypress/template.manifest.json +74 -0
- package/dist/templates/runtime-bundles/shared-kernel/STAGES.md +2 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -40502,12 +40502,11 @@ function listPm2Processes() {
|
|
|
40502
40502
|
return void 0;
|
|
40503
40503
|
}
|
|
40504
40504
|
}
|
|
40505
|
-
function detectForeignPm2(
|
|
40505
|
+
function detectForeignPm2(_projectName, agentId, procs) {
|
|
40506
40506
|
if (!procs) return void 0;
|
|
40507
|
-
const slotPrefix = `har-${projectName}-agent-${agentId}-`;
|
|
40508
40507
|
const legacyPrefix = `agent-${agentId}-`;
|
|
40509
40508
|
const foreign = procs.filter(
|
|
40510
|
-
(p) => p.name &&
|
|
40509
|
+
(p) => p.name && p.name.startsWith(legacyPrefix) && !p.name.startsWith("har-")
|
|
40511
40510
|
);
|
|
40512
40511
|
if (foreign.length === 0) return void 0;
|
|
40513
40512
|
return {
|
|
@@ -40517,6 +40516,48 @@ function detectForeignPm2(projectName, agentId, procs) {
|
|
|
40517
40516
|
}))
|
|
40518
40517
|
};
|
|
40519
40518
|
}
|
|
40519
|
+
function freeAgentSlots(harnessRoot, projectName, agentId, procs) {
|
|
40520
|
+
let ids = [];
|
|
40521
|
+
let range;
|
|
40522
|
+
try {
|
|
40523
|
+
range = getAgentSlotRange(harnessRoot);
|
|
40524
|
+
ids = getAgentSlotIds(harnessRoot);
|
|
40525
|
+
} catch {
|
|
40526
|
+
return { free: [] };
|
|
40527
|
+
}
|
|
40528
|
+
const free = ids.filter((id) => {
|
|
40529
|
+
if (id === agentId) return false;
|
|
40530
|
+
const session = readSlotRegistry(harnessRoot, id);
|
|
40531
|
+
if (session !== void 0 && session.status !== "completed") return false;
|
|
40532
|
+
return detectForeignPm2(projectName, id, procs) === void 0;
|
|
40533
|
+
});
|
|
40534
|
+
return { free, range };
|
|
40535
|
+
}
|
|
40536
|
+
function foreignPm2Advice(agentId, processes, free, range) {
|
|
40537
|
+
const cwds = [
|
|
40538
|
+
...new Set(processes.map((p) => p.cwd).filter((cwd) => Boolean(cwd)))
|
|
40539
|
+
];
|
|
40540
|
+
const rangeLabel = range ? `agentSlots ${range.min}\u2013${range.max}` : "agentSlots in .har/stages.json";
|
|
40541
|
+
const higher = free.filter((id) => id > agentId);
|
|
40542
|
+
const suggested = higher[0] ?? free[0];
|
|
40543
|
+
const lines = [];
|
|
40544
|
+
if (suggested !== void 0) {
|
|
40545
|
+
const kind = higher.length > 0 ? "higher slot" : "slot";
|
|
40546
|
+
lines.push(
|
|
40547
|
+
`Use a free ${kind}: har env launch ${suggested} (free: ${free.join(", ")}; ${rangeLabel}).`
|
|
40548
|
+
);
|
|
40549
|
+
} else if (range) {
|
|
40550
|
+
lines.push(`No free slot in ${rangeLabel}. Raise agentSlots.max in .har/stages.json.`);
|
|
40551
|
+
} else {
|
|
40552
|
+
lines.push("Use a different agent slot (see agentSlots in .har/stages.json).");
|
|
40553
|
+
}
|
|
40554
|
+
lines.push(
|
|
40555
|
+
"These processes belong to another project and must not be deleted from this checkout."
|
|
40556
|
+
);
|
|
40557
|
+
const where = cwds.length === 0 ? "that project checkout (cwd unknown)" : cwds.join(", ");
|
|
40558
|
+
lines.push(`To stop that other session, run \`har env teardown ${agentId}\` in ${where}.`);
|
|
40559
|
+
return { lines, suggested };
|
|
40560
|
+
}
|
|
40520
40561
|
function infraEnabled2(env3, service) {
|
|
40521
40562
|
return ` ${env3.HARNESS_INFRA_SERVICES ?? ""} `.includes(` ${service} `);
|
|
40522
40563
|
}
|
|
@@ -40570,12 +40611,17 @@ function inspectSlotReadiness(repoPath, agentId, options = {}) {
|
|
|
40570
40611
|
const foreign = usesPm2 ? detectForeignPm2(projectName, agentId, pm2Procs) : void 0;
|
|
40571
40612
|
if (foreign) {
|
|
40572
40613
|
const names = foreign.processes.map((p) => p.name).join(", ");
|
|
40614
|
+
const slots = freeAgentSlots(harnessRoot, projectName, agentId, pm2Procs);
|
|
40615
|
+
const advice = foreignPm2Advice(agentId, foreign.processes, slots.free, slots.range);
|
|
40573
40616
|
blockers.push({
|
|
40574
40617
|
code: "foreign_pm2",
|
|
40575
40618
|
message: `Foreign PM2 processes match agent ${agentId}: ${names}`,
|
|
40576
|
-
remediation: "
|
|
40577
|
-
details: { processes: foreign.processes }
|
|
40619
|
+
remediation: advice.lines.join(" "),
|
|
40620
|
+
details: { processes: foreign.processes, adviceLines: advice.lines }
|
|
40578
40621
|
});
|
|
40622
|
+
if (advice.suggested !== void 0) {
|
|
40623
|
+
remediations.push(`har env launch ${advice.suggested}`);
|
|
40624
|
+
}
|
|
40579
40625
|
remediations.push(`Inspect with: ${packageRunner()} pm2 jlist | grep agent-${agentId}`);
|
|
40580
40626
|
}
|
|
40581
40627
|
if (usesPm2 && pm2Procs) {
|
|
@@ -40728,10 +40774,14 @@ function bashBlockerLines(agentId, b2) {
|
|
|
40728
40774
|
switch (b2.code) {
|
|
40729
40775
|
case "foreign_pm2": {
|
|
40730
40776
|
const procs = details.processes ?? [];
|
|
40777
|
+
const advice = Array.isArray(details.adviceLines) ? details.adviceLines : [
|
|
40778
|
+
"Use a different agent slot.",
|
|
40779
|
+
"These processes belong to another project and must not be deleted from this checkout."
|
|
40780
|
+
];
|
|
40731
40781
|
return [
|
|
40732
40782
|
`ERROR: foreign PM2 processes match agent ${agentId}:`,
|
|
40733
40783
|
...procs.map((p) => ` ${p.name} cwd=${p.cwd ?? "unknown"}`),
|
|
40734
|
-
|
|
40784
|
+
...advice.map((line) => ` ${line}`)
|
|
40735
40785
|
];
|
|
40736
40786
|
}
|
|
40737
40787
|
case "control_port_conflict":
|
|
@@ -40834,6 +40884,7 @@ var init_slot_preflight = __esm({
|
|
|
40834
40884
|
init_slot_launch_guard_occupied();
|
|
40835
40885
|
init_slot_ports();
|
|
40836
40886
|
init_capabilities();
|
|
40887
|
+
init_stages();
|
|
40837
40888
|
init_slot_registry();
|
|
40838
40889
|
init_worktree_untracked();
|
|
40839
40890
|
import_child_process11 = require("child_process");
|
|
@@ -42500,7 +42551,7 @@ function detectPm2Issue(projectName, agentId, session, procs) {
|
|
|
42500
42551
|
const legacyPrefix = `agent-${agentId}-`;
|
|
42501
42552
|
const owned = procs.filter((p) => p.name?.startsWith(slotPrefix));
|
|
42502
42553
|
const foreign = procs.filter(
|
|
42503
|
-
(p) => p.name &&
|
|
42554
|
+
(p) => p.name && p.name.startsWith(legacyPrefix) && !p.name.startsWith("har-")
|
|
42504
42555
|
);
|
|
42505
42556
|
if (foreign.length > 0) return "foreign_pm2";
|
|
42506
42557
|
if (owned.length > 0) {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Cypress end-to-end tests — official GitHub Action
|
|
2
|
+
# https://github.com/cypress-io/github-action
|
|
3
|
+
#
|
|
4
|
+
# Adapt `start` and `wait-on` to this repo's dev server. The action installs
|
|
5
|
+
# dependencies, downloads the Cypress binary, and runs `cypress run`.
|
|
6
|
+
name: Cypress tests
|
|
7
|
+
|
|
8
|
+
on:
|
|
9
|
+
push:
|
|
10
|
+
pull_request:
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
cypress-run:
|
|
14
|
+
runs-on: ubuntu-24.04
|
|
15
|
+
steps:
|
|
16
|
+
- name: Checkout
|
|
17
|
+
uses: actions/checkout@v4
|
|
18
|
+
- name: Cypress run
|
|
19
|
+
uses: cypress-io/github-action@v7
|
|
20
|
+
with:
|
|
21
|
+
start: npm run dev
|
|
22
|
+
wait-on: http://localhost:3000
|
|
23
|
+
browser: chrome
|
|
24
|
+
env:
|
|
25
|
+
CYPRESS_BASE_URL: http://localhost:3000
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Cypress (cypress-e2e)
|
|
2
|
+
|
|
3
|
+
Adapt specs under `cypress/e2e/` for your application. Full verification (`verify --full`) runs this stage when it is listed in `verificationStages` (the plugin adds it).
|
|
4
|
+
|
|
5
|
+
| Path | Purpose |
|
|
6
|
+
|------|---------|
|
|
7
|
+
| `cypress/e2e/` | End-to-end specs — extend when you add UI |
|
|
8
|
+
| `cypress/support/e2e.js` | Shared commands loaded before every spec |
|
|
9
|
+
| `cypress.config.js` | Base URL, spec pattern, artifact folders |
|
|
10
|
+
|
|
11
|
+
## Run
|
|
12
|
+
|
|
13
|
+
After `har env launch <id>` and `npm install`:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx cypress verify
|
|
17
|
+
./.har/stages/cypress-e2e.sh <id>
|
|
18
|
+
# included in:
|
|
19
|
+
har env verify <id> --full
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`cypress run` is headless. Cypress 16 deprecates the bundled Electron browser, so the stage uses Chrome when `google-chrome` is on PATH (or Google Chrome.app on macOS), then Chromium, then Electron. Set `HARNESS_CYPRESS_BROWSER` (`chrome`, `chromium`, `firefox`, `electron`) to override. Electron still runs in Cypress 16 and prints a deprecation warning.
|
|
23
|
+
|
|
24
|
+
## Ports
|
|
25
|
+
|
|
26
|
+
The stage reads the slot env file and points Cypress at that slot:
|
|
27
|
+
|
|
28
|
+
| Variable | Source | Cypress |
|
|
29
|
+
|----------|--------|---------|
|
|
30
|
+
| Frontend | `FE_PORT` (or `HARNESS_FE_BASE_PORT + id * 10`) | `CYPRESS_BASE_URL` → `cy.visit('/')` |
|
|
31
|
+
| API | `API_PORT` (or `HARNESS_API_BASE_PORT + id * 10`) | `CYPRESS_API_URL` → `Cypress.expose('API_URL')` |
|
|
32
|
+
| Health path | `HARNESS_HEALTH_CHECK_PATH` (default `/health`) | `Cypress.expose('HEALTH_PATH')` |
|
|
33
|
+
|
|
34
|
+
Do not hardcode slot ports in specs. A monolith that serves UI and API on one port should set both bases to that origin in `.har/harness.env` (or only assert against `baseUrl`).
|
|
35
|
+
|
|
36
|
+
The scaffold `api.cy.js` expects `GET /health` to return 200. Change the path, or set `HARNESS_HEALTH_CHECK_PATH`, to match your app.
|
|
37
|
+
|
|
38
|
+
## Artifacts
|
|
39
|
+
|
|
40
|
+
Written under the main repo's `.har/artifacts/cypress-e2e/` (not the worktree):
|
|
41
|
+
|
|
42
|
+
| Path | Contents |
|
|
43
|
+
|------|----------|
|
|
44
|
+
| `cypress.log` | Full `cypress run` output |
|
|
45
|
+
| `screenshots/` | Automatic failure screenshots |
|
|
46
|
+
| `videos/` | Present when you set `video: true` in `cypress.config.js` |
|
|
47
|
+
|
|
48
|
+
Video recording is off by default (`video: false`), matching current Cypress. Turn it on in config when you want a video per spec.
|
|
49
|
+
|
|
50
|
+
Exit codes use `--posix-exit-codes`: `0` when every spec passes, `1` when Cypress fails. The stage propagates that code. Without the flag, Cypress exits with the number of failed tests.
|
|
51
|
+
|
|
52
|
+
## Linux libraries
|
|
53
|
+
|
|
54
|
+
`npx cypress verify` checks that the Electron binary can start. If it reports missing libraries, install the packages listed in the [Cypress Linux prerequisites](https://docs.cypress.io/app/get-started/install-cypress#Linux-Prerequisites) and verify again.
|
|
55
|
+
|
|
56
|
+
## Existing Cypress projects
|
|
57
|
+
|
|
58
|
+
`add-plugin` will not overwrite a `cypress.config.js` you already have. Point `specPattern` at your specs, keep artifact folders under `.har/artifacts/cypress-e2e/`, and let the stage export `CYPRESS_BASE_URL` rather than a fixed `baseUrl`.
|
|
59
|
+
|
|
60
|
+
## New UI features
|
|
61
|
+
|
|
62
|
+
Add or update a spec so `cypress-e2e` covers the change. Prefer one file per feature under `cypress/e2e/<feature>.cy.js`. Full verification must pass before done.
|
|
63
|
+
|
|
64
|
+
## CI
|
|
65
|
+
|
|
66
|
+
`har env add-plugin cypress --with-ci` copies `.github/workflows/cypress.yml`, the official `cypress-io/github-action@v7` recipe. Change `start` and `wait-on` to this repo's dev command and URL.
|
|
67
|
+
|
|
68
|
+
## Plugin updates
|
|
69
|
+
|
|
70
|
+
When HAR ships a new plugin template version, merge drift from:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
har env maintain
|
|
74
|
+
# review .har/maintain/plugins/cypress/
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Or refresh all plugin-owned files:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
har env add-plugin cypress --force
|
|
81
|
+
```
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Cypress end-to-end tests for an agent slot.
|
|
3
|
+
# Outputs JSON to stdout, human-readable progress to stderr.
|
|
4
|
+
#
|
|
5
|
+
# Usage: ./.har/stages/cypress-e2e.sh <agent-id>
|
|
6
|
+
# Prerequisite: har env launch <agent-id> AND npm install (Cypress binary)
|
|
7
|
+
# See: ./.har/stages/CYPRESS.md
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
|
|
10
|
+
# 1.0 stage surface: the runner exports WORK_DIR, ENV_FILE, AGENT_ID and
|
|
11
|
+
# HAR_HARNESS_DIR, with harness.env and the slot env file already sourced —
|
|
12
|
+
# agent-slot.sh is retired (1.0 migration).
|
|
13
|
+
HARNESS_DIR="${HAR_HARNESS_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
|
|
14
|
+
|
|
15
|
+
AGENT_ID="${1:-${AGENT_ID:?Usage: cypress-e2e.sh <agent-id>}}"
|
|
16
|
+
|
|
17
|
+
# Direct invocation (the manifest nextSteps) has no runner in front of it.
|
|
18
|
+
# Recover the slot work dir from the registry launch wrote.
|
|
19
|
+
if [ -z "${ENV_FILE:-}" ] || [ -z "${WORK_DIR:-}" ]; then
|
|
20
|
+
SLOT_FILE="$HARNESS_DIR/slots/agent-${AGENT_ID}.json"
|
|
21
|
+
if [ -f "$SLOT_FILE" ]; then
|
|
22
|
+
SLOT_WORK="$(node -e 'const fs=require("fs"); const s=JSON.parse(fs.readFileSync(process.argv[1],"utf8")); process.stdout.write(s.workDir||"")' "$SLOT_FILE" 2>/dev/null || true)"
|
|
23
|
+
if [ -n "$SLOT_WORK" ] && [ -f "$SLOT_WORK/.env.agent.${AGENT_ID}" ]; then
|
|
24
|
+
WORK_DIR="$SLOT_WORK"
|
|
25
|
+
ENV_FILE="$SLOT_WORK/.env.agent.${AGENT_ID}"
|
|
26
|
+
set -a
|
|
27
|
+
# shellcheck disable=SC1091
|
|
28
|
+
[ -f "$HARNESS_DIR/harness.env" ] && . "$HARNESS_DIR/harness.env"
|
|
29
|
+
# shellcheck disable=SC1090
|
|
30
|
+
. "$ENV_FILE"
|
|
31
|
+
set +a
|
|
32
|
+
fi
|
|
33
|
+
fi
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
now_ms() { node -e 'process.stdout.write(String(Date.now()))' 2>/dev/null || echo 0; }
|
|
37
|
+
|
|
38
|
+
log() { echo "==> [cypress-e2e agent-$AGENT_ID] $*" >&2; }
|
|
39
|
+
|
|
40
|
+
ENV_FILE="${ENV_FILE:?No slot env for agent ${AGENT_ID} — run har env launch ${AGENT_ID} first}"
|
|
41
|
+
WORK_DIR="${WORK_DIR:?No slot work dir for agent ${AGENT_ID} — run har env launch ${AGENT_ID} first}"
|
|
42
|
+
|
|
43
|
+
FE_PORT="${FE_PORT:-$(( ${HARNESS_FE_BASE_PORT:-3000} + AGENT_ID * 10 ))}"
|
|
44
|
+
API_PORT="${API_PORT:-$(( ${HARNESS_API_BASE_PORT:-8000} + AGENT_ID * 10 ))}"
|
|
45
|
+
|
|
46
|
+
export BASE_URL="${BASE_URL:-http://localhost:${FE_PORT}}"
|
|
47
|
+
export API_URL="${API_URL:-http://localhost:${API_PORT}}"
|
|
48
|
+
export CYPRESS_BASE_URL="${CYPRESS_BASE_URL:-$BASE_URL}"
|
|
49
|
+
export CYPRESS_API_URL="${CYPRESS_API_URL:-$API_URL}"
|
|
50
|
+
HEALTH_PATH="${HARNESS_HEALTH_CHECK_PATH:-${HARNESS_HEALTH_PATH:-/health}}"
|
|
51
|
+
export CYPRESS_HEALTH_PATH="${CYPRESS_HEALTH_PATH:-$HEALTH_PATH}"
|
|
52
|
+
|
|
53
|
+
# Artifacts land next to this harness (main checkout), not inside a worktree
|
|
54
|
+
# whose .env.agent file may rewrite REPO_ROOT.
|
|
55
|
+
ARTIFACT_DIR="$(cd "$HARNESS_DIR/.." && pwd)/.har/artifacts/cypress-e2e"
|
|
56
|
+
mkdir -p "$ARTIFACT_DIR"
|
|
57
|
+
export HARNESS_CYPRESS_ARTIFACT_DIR="$ARTIFACT_DIR"
|
|
58
|
+
|
|
59
|
+
# Cypress 16 deprecates the bundled Electron browser. Prefer an installed Chrome
|
|
60
|
+
# or Chromium; fall back to Electron so `npm install` is still enough to run.
|
|
61
|
+
if [ -n "${HARNESS_CYPRESS_BROWSER:-}" ]; then
|
|
62
|
+
BROWSER="$HARNESS_CYPRESS_BROWSER"
|
|
63
|
+
elif command -v google-chrome >/dev/null 2>&1 || command -v google-chrome-stable >/dev/null 2>&1 || [ -d "/Applications/Google Chrome.app" ]; then
|
|
64
|
+
BROWSER="chrome"
|
|
65
|
+
elif command -v chromium >/dev/null 2>&1 || command -v chromium-browser >/dev/null 2>&1; then
|
|
66
|
+
BROWSER="chromium"
|
|
67
|
+
else
|
|
68
|
+
BROWSER="electron"
|
|
69
|
+
fi
|
|
70
|
+
export HARNESS_CYPRESS_BROWSER="$BROWSER"
|
|
71
|
+
|
|
72
|
+
if [ ! -x "$WORK_DIR/node_modules/.bin/cypress" ]; then
|
|
73
|
+
echo "Error: Cypress is not installed in $WORK_DIR." >&2
|
|
74
|
+
echo " npm install" >&2
|
|
75
|
+
echo " npx cypress verify" >&2
|
|
76
|
+
echo " Then re-run: ./.har/stages/cypress-e2e.sh ${AGENT_ID}" >&2
|
|
77
|
+
exit 1
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
log "Running Cypress ($BROWSER) against $CYPRESS_BASE_URL (API: $CYPRESS_API_URL)"
|
|
81
|
+
log "Work dir: $WORK_DIR"
|
|
82
|
+
log "Artifacts: $ARTIFACT_DIR"
|
|
83
|
+
|
|
84
|
+
START_TOTAL=$(now_ms)
|
|
85
|
+
LOG_FILE="$ARTIFACT_DIR/cypress.log"
|
|
86
|
+
|
|
87
|
+
set +e
|
|
88
|
+
(
|
|
89
|
+
cd "$WORK_DIR"
|
|
90
|
+
npx cypress run \
|
|
91
|
+
--e2e \
|
|
92
|
+
--browser "$BROWSER" \
|
|
93
|
+
--posix-exit-codes
|
|
94
|
+
) >"$LOG_FILE" 2>&1
|
|
95
|
+
CYPRESS_EXIT=$?
|
|
96
|
+
set -e
|
|
97
|
+
|
|
98
|
+
END_TOTAL=$(now_ms)
|
|
99
|
+
TOTAL_MS=$(( END_TOTAL - START_TOTAL ))
|
|
100
|
+
|
|
101
|
+
if [ "$CYPRESS_EXIT" -eq 0 ]; then
|
|
102
|
+
log "Cypress passed (${TOTAL_MS}ms)"
|
|
103
|
+
else
|
|
104
|
+
log "Cypress failed with exit $CYPRESS_EXIT (${TOTAL_MS}ms) — see $LOG_FILE"
|
|
105
|
+
tail -30 "$LOG_FILE" | sed 's/^/ /' >&2
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
HARNESS_CYPRESS_ARTIFACT_DIR="$ARTIFACT_DIR" node -e "
|
|
109
|
+
const fs = require('fs');
|
|
110
|
+
const artifactDir = process.env.HARNESS_CYPRESS_ARTIFACT_DIR;
|
|
111
|
+
const out = {
|
|
112
|
+
status: ${CYPRESS_EXIT} === 0 ? 'pass' : 'fail',
|
|
113
|
+
stageId: 'cypress-e2e',
|
|
114
|
+
kind: 'test',
|
|
115
|
+
agent_id: ${AGENT_ID},
|
|
116
|
+
total_ms: ${TOTAL_MS},
|
|
117
|
+
exit_code: ${CYPRESS_EXIT},
|
|
118
|
+
browser: process.env.HARNESS_CYPRESS_BROWSER || 'electron',
|
|
119
|
+
urls: [
|
|
120
|
+
{ label: 'frontend', url: process.env.CYPRESS_BASE_URL || '' },
|
|
121
|
+
{ label: 'api', url: process.env.CYPRESS_API_URL || '' },
|
|
122
|
+
],
|
|
123
|
+
artifacts: [
|
|
124
|
+
{ path: '.har/artifacts/cypress-e2e', kind: 'directory' },
|
|
125
|
+
{ path: '.har/artifacts/cypress-e2e/cypress.log', kind: 'log' },
|
|
126
|
+
],
|
|
127
|
+
};
|
|
128
|
+
if (fs.existsSync(artifactDir + '/screenshots')) {
|
|
129
|
+
out.artifacts.push({ path: '.har/artifacts/cypress-e2e/screenshots', kind: 'directory' });
|
|
130
|
+
}
|
|
131
|
+
process.stdout.write(JSON.stringify(out, null, 2) + '\n');
|
|
132
|
+
"
|
|
133
|
+
|
|
134
|
+
exit "$CYPRESS_EXIT"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Cypress HAR plugin
|
|
2
|
+
|
|
3
|
+
This plugin was added by `har env add-plugin cypress`. It registers a `cypress-e2e` stage and scaffolds a minimal Cypress end-to-end suite.
|
|
4
|
+
|
|
5
|
+
## Next steps
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install
|
|
9
|
+
npx cypress verify
|
|
10
|
+
har env launch 1
|
|
11
|
+
./.har/stages/cypress-e2e.sh 1
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
See `.har/stages/CYPRESS.md` for adaptation checklist, ports, and artifact layout.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// TODO: adapt the path for your app's health endpoint.
|
|
2
|
+
// Public config comes from cypress.config.js `expose` (Cypress.env was removed in Cypress 16).
|
|
3
|
+
describe('api health', () => {
|
|
4
|
+
it('health endpoint responds', () => {
|
|
5
|
+
const healthPath = Cypress.expose('HEALTH_PATH') || '/health';
|
|
6
|
+
const apiUrl = String(Cypress.expose('API_URL') || Cypress.config('baseUrl') || '').replace(
|
|
7
|
+
/\/$/,
|
|
8
|
+
'',
|
|
9
|
+
);
|
|
10
|
+
const requestPath = String(healthPath).startsWith('/') ? String(healthPath) : `/${healthPath}`;
|
|
11
|
+
cy.request(`${apiUrl}${requestPath}`).its('status').should('eq', 200);
|
|
12
|
+
});
|
|
13
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
// Loaded before every spec. Add app-wide commands here.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Cypress config for the HAR `cypress-e2e` verification stage.
|
|
2
|
+
//
|
|
3
|
+
// Harness integration
|
|
4
|
+
// -------------------
|
|
5
|
+
// - Stage id: `cypress-e2e` (`.har/stages/cypress-e2e.sh`, registered in `stages.json`).
|
|
6
|
+
// - Runs on FULL verify only — not on quick verify:
|
|
7
|
+
// har env verify <id> --full
|
|
8
|
+
// - Quick verify stops at the quick-tier stages (typecheck, unit tests, api-health).
|
|
9
|
+
//
|
|
10
|
+
// Prerequisites
|
|
11
|
+
// -------------
|
|
12
|
+
// 1. Launch a slot first: har env launch <id>
|
|
13
|
+
// 2. Install Cypress: npm install && npx cypress verify
|
|
14
|
+
// The Cypress npm package downloads the Electron binary. On Linux, system
|
|
15
|
+
// libraries from the Cypress docs may also be required.
|
|
16
|
+
//
|
|
17
|
+
// Environment (injected by cypress-e2e.sh — never hardcode slot ports in specs)
|
|
18
|
+
// ------------------------------------------------------------------------------
|
|
19
|
+
// CYPRESS_BASE_URL Frontend origin for cy.visit('/') (slot FE port)
|
|
20
|
+
// CYPRESS_API_URL API origin (Cypress.expose('API_URL'); Cypress.env was removed in Cypress 16)
|
|
21
|
+
// CYPRESS_HEALTH_PATH Health path (Cypress.expose('HEALTH_PATH'), default /health)
|
|
22
|
+
// HARNESS_CYPRESS_ARTIFACT_DIR Absolute artifact directory for this run
|
|
23
|
+
// HARNESS_CYPRESS_BROWSER Browser passed to `cypress run` (chrome when installed, else electron)
|
|
24
|
+
//
|
|
25
|
+
// Test layout — agents must add or update specs for every UI change
|
|
26
|
+
// ------------------------------------------------------------------
|
|
27
|
+
// cypress/e2e/<feature>.cy.js End-to-end flows (prefer one file per feature)
|
|
28
|
+
//
|
|
29
|
+
// Full verify only proves existing specs pass. New UI behavior needs new or
|
|
30
|
+
// updated specs here — the harness does not auto-generate flows per feature.
|
|
31
|
+
// Widen `specPattern` to `cypress/e2e/**/*.cy.{js,jsx,ts,tsx}` when you add
|
|
32
|
+
// TypeScript specs.
|
|
33
|
+
//
|
|
34
|
+
// Artifacts (gitignored under .har/artifacts/)
|
|
35
|
+
// --------------------------------------------
|
|
36
|
+
// cypress-e2e/cypress.log Runner log
|
|
37
|
+
// cypress-e2e/screenshots/ Failure screenshots (cypress run)
|
|
38
|
+
// cypress-e2e/videos/ Videos when `video: true` (off by default)
|
|
39
|
+
//
|
|
40
|
+
// Local-only (without cypress-e2e.sh): npm run test:cypress
|
|
41
|
+
// Set CYPRESS_BASE_URL (and CYPRESS_API_URL if split) when the app is not on
|
|
42
|
+
// the default below.
|
|
43
|
+
const path = require('path');
|
|
44
|
+
const { defineConfig } = require('cypress');
|
|
45
|
+
|
|
46
|
+
const baseUrl = process.env.CYPRESS_BASE_URL || process.env.BASE_URL || 'http://localhost:3000';
|
|
47
|
+
const apiUrl = process.env.CYPRESS_API_URL || process.env.API_URL || baseUrl;
|
|
48
|
+
const healthPath = process.env.CYPRESS_HEALTH_PATH || '/health';
|
|
49
|
+
const artifactDir =
|
|
50
|
+
process.env.HARNESS_CYPRESS_ARTIFACT_DIR || path.join('.har', 'artifacts', 'cypress-e2e');
|
|
51
|
+
|
|
52
|
+
module.exports = defineConfig({
|
|
53
|
+
expose: {
|
|
54
|
+
API_URL: apiUrl,
|
|
55
|
+
HEALTH_PATH: healthPath,
|
|
56
|
+
},
|
|
57
|
+
e2e: {
|
|
58
|
+
baseUrl,
|
|
59
|
+
specPattern: 'cypress/e2e/**/*.cy.js',
|
|
60
|
+
supportFile: 'cypress/support/e2e.js',
|
|
61
|
+
fixturesFolder: false,
|
|
62
|
+
screenshotsFolder: path.join(artifactDir, 'screenshots'),
|
|
63
|
+
videosFolder: path.join(artifactDir, 'videos'),
|
|
64
|
+
downloadsFolder: path.join(artifactDir, 'downloads'),
|
|
65
|
+
video: false,
|
|
66
|
+
screenshotOnRunFailure: true,
|
|
67
|
+
},
|
|
68
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "cypress",
|
|
3
|
+
"stageId": "cypress-e2e",
|
|
4
|
+
"verificationStages": [
|
|
5
|
+
"cypress-e2e"
|
|
6
|
+
],
|
|
7
|
+
"stage": {
|
|
8
|
+
"id": "cypress-e2e",
|
|
9
|
+
"kind": "test",
|
|
10
|
+
"description": "Cypress end-to-end tests against the slot's running app",
|
|
11
|
+
"script": "stages/cypress-e2e.sh",
|
|
12
|
+
"requiresAgentId": true,
|
|
13
|
+
"artifacts": [
|
|
14
|
+
{
|
|
15
|
+
"path": ".har/artifacts/cypress-e2e",
|
|
16
|
+
"kind": "directory",
|
|
17
|
+
"description": "Cypress run log, screenshots, and videos"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"path": ".har/artifacts/cypress-e2e/cypress.log",
|
|
21
|
+
"kind": "log"
|
|
22
|
+
}
|
|
23
|
+
],
|
|
24
|
+
"tier": "full"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
{
|
|
28
|
+
"src": ".har/stages/cypress-e2e.sh",
|
|
29
|
+
"dest": ".har/stages/cypress-e2e.sh",
|
|
30
|
+
"executable": true
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"src": ".har/stages/CYPRESS.md",
|
|
34
|
+
"dest": ".har/stages/CYPRESS.md"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"src": "cypress.config.js",
|
|
38
|
+
"dest": "cypress.config.js"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"src": "cypress/e2e/smoke.cy.js",
|
|
42
|
+
"dest": "cypress/e2e/smoke.cy.js"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"src": "cypress/e2e/api.cy.js",
|
|
46
|
+
"dest": "cypress/e2e/api.cy.js"
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"src": "cypress/support/e2e.js",
|
|
50
|
+
"dest": "cypress/support/e2e.js"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"src": "README.md",
|
|
54
|
+
"dest": "cypress/README.md"
|
|
55
|
+
}
|
|
56
|
+
],
|
|
57
|
+
"optionalFiles": [
|
|
58
|
+
{
|
|
59
|
+
"src": ".github/workflows/cypress.yml",
|
|
60
|
+
"dest": ".github/workflows/cypress.yml",
|
|
61
|
+
"skipFlag": "skipCi"
|
|
62
|
+
}
|
|
63
|
+
],
|
|
64
|
+
"merge": {
|
|
65
|
+
"package.json": "package.fragment.json"
|
|
66
|
+
},
|
|
67
|
+
"nextSteps": [
|
|
68
|
+
"npm install",
|
|
69
|
+
"npx cypress verify",
|
|
70
|
+
"har env launch 1",
|
|
71
|
+
"./.har/stages/cypress-e2e.sh 1"
|
|
72
|
+
],
|
|
73
|
+
"docsPath": ".har/stages/CYPRESS.md"
|
|
74
|
+
}
|
|
@@ -104,7 +104,8 @@ committed.
|
|
|
104
104
|
playwright` (web), `har env add-plugin rocketsim` (iOS), or `har env
|
|
105
105
|
add-plugin kerno` (backend), `har env add-plugin gitleaks` (secrets scanning, any
|
|
106
106
|
stack), or `har env add-plugin trivy` (dependency + IaC security scan, any stack),
|
|
107
|
-
or `har env add-plugin semgrep` (SAST, any stack)
|
|
107
|
+
or `har env add-plugin semgrep` (SAST, any stack),
|
|
108
|
+
or `har env add-plugin cypress` (browser e2e)
|
|
108
109
|
installs one. A
|
|
109
110
|
plugin is just packaging: it copies files, merges `package.json` fragments,
|
|
110
111
|
and registers stages through the exact same registry as everything else.
|