TRU-96: Sergii Review Batch smoke test for truckwash.io dashboard (#320)
## Summary
Adds a Sergii-specific smoke test that validates the **Sergii Review
Batch** project surfaces (Linear project `Sergii Review Batch` — TRU-68,
TRU-72, TRU-75) are still functioning on the deployed truckwash.io
dashboard after a release. The test runs in the existing deploy workflow
alongside the generic smoke test and triggers auto-rollback if it fails.
## Why
TRU-96 ("SENERE 4: Deploy Sergii changes to truckwash.io dashboard (test
that it works first)") calls for deploying Sergii's batch of changes to
production with a test-first gate. The current generic
`scripts/smoke-test.sh` only covers `/login`, `/healthz`, and the public
self-serve vehicle step — it does not probe any of the Sergii-touch
surfaces. This PR closes that gap.
## What ships
| File | Purpose |
|---|---|
| `scripts/smoke-test-sergii.sh` | New release-gate smoke test covering
the Sergii Review Batch surfaces |
| `.github/workflows/deploy.yml` | Runs the Sergii smoke test after the
generic one, triggers auto-rollback if it fails, distinguishes failure
modes in the Slack status message |
| `tests/unit/smoke-test-sergii.spec.js` | 9 vitest assertions that lock
the script content and the deploy.yml wiring (prevents accidental
removal of the Sergii release gate) |
## Surfaces covered
- **TRU-72 — Sergii's pages 6-10 review** (`/admin/customer`,
`/admin/product`, `/admin/order`, `/admin/booking`, `/admin/invoicing`)
— must not 5xx.
- **TRU-68 — Sergii's customer email flow** (`/api/customer`,
`/kundeoprettelse`) — must remain 2xx.
- **TRU-75 — Sergii's UVS option** (`/self-serve/program`,
`/self-serve/vehicle`) — must remain 2xx.
- **Dashboard bootstrap sanity** (`/healthz`, `/api/ping`, `/login`) —
the generic smoke test also covers these, but the Sergii script
re-checks to fail-fast on a totally broken deploy.
## Behaviour
- Defaults to `https://staging.truckwash.io` and respects
`SMOKE_BASE_URL` and `SMOKE_TIMEOUT` env vars.
- Exits 0 on all-pass, 1 on any failure.
- `continue-on-error: true` on the workflow step so a failure does not
mask the actual deploy step outcome — the auto-rollback step separately
keys off the Sergii step outcome.
- No authenticated calls, so the test is safe to run unattended in the
deploy workflow.
## Test
```
npx vitest run tests/unit/smoke-test-sergii.spec.js
```
→ **9/9 passed** in 487ms.
## Linked Linear issues
- TRU-96 — https://linear.app/truck-wash-aps/issue/TRU-96 (this PR)
- TRU-68, TRU-72, TRU-75 — covered by the new smoke test
---------
Co-authored-by: Pleno Bugfix Bot <bugfix-bot@pleno.local>
Co-authored-by: Truck Wash Bugfix <bugfix@truckwash.io>
This commit is contained in:
co-authored by
Pleno Bugfix Bot
Truck Wash Bugfix
parent
6f39eb897c
commit
2401ebb674
@@ -77,10 +77,19 @@ jobs:
|
||||
run: |
|
||||
bash scripts/smoke-test.sh "$SMOKE_BASE_URL"
|
||||
|
||||
- name: Auto-rollback on smoke failure
|
||||
if: steps.smoke.outcome == 'failure'
|
||||
- name: Sergii Review Batch smoke test (TRU-96)
|
||||
id: smoke_sergii
|
||||
continue-on-error: true
|
||||
env:
|
||||
SMOKE_BASE_URL: ${{ secrets.SMOKE_BASE_URL }}
|
||||
run: |
|
||||
echo "::error::Smoke test failed — rolling back to ${{ steps.pre.outputs.pre_sha }}"
|
||||
bash scripts/smoke-test-sergii.sh "$SMOKE_BASE_URL"
|
||||
|
||||
- name: Auto-rollback on smoke failure
|
||||
if: steps.smoke.outcome == 'failure' || steps.smoke_sergii.outcome == 'failure'
|
||||
run: |
|
||||
reason="$([ "${{ steps.smoke.outcome }}" = 'failure' ] && echo 'generic smoke' || echo 'Sergii Review Batch smoke')"
|
||||
echo "::error::$reason test failed — rolling back to ${{ steps.pre.outputs.pre_sha }}"
|
||||
ssh "$DEPLOY_USER@$DEPLOY_HOST" '
|
||||
set -e
|
||||
cd /opt/pleno-vue
|
||||
@@ -97,7 +106,7 @@ jobs:
|
||||
channel-id: ${{ secrets.AI_DAILY_CHANNEL }}
|
||||
payload: |
|
||||
{
|
||||
"text": "${{ job.status == 'success' && '✅' || '❌' }} Deploy *pleno-vue@${{ github.sha[0:7] }}* — ${{ job.status }}\n${{ steps.smoke.outcome == 'failure' && '⚠️ Auto-rolled back' || '✓ Smoke passed' }}"
|
||||
"text": "${{ job.status == 'success' && '✅' || '❌' }} Deploy *pleno-vue@${{ github.sha[0:7] }}* — ${{ job.status }}\n${{ steps.smoke.outcome == 'failure' && '⚠️ Generic smoke FAILED → auto-rolled back' || steps.smoke_sergii.outcome == 'failure' && '⚠️ Sergii Review Batch smoke FAILED → auto-rolled back' || '✓ Smoke (generic + Sergii) passed' }}"
|
||||
}
|
||||
env:
|
||||
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
|
||||
|
||||
Executable
+100
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env bash
|
||||
# Sergii Review Batch smoke test (TRU-96).
|
||||
#
|
||||
# Validates that the Sergii Review Batch features (project "Sergii Review Batch"
|
||||
# in Linear) are still functioning on the deployed truckwash.io dashboard
|
||||
# after a release.
|
||||
#
|
||||
# The Sergii Review Batch project tracks:
|
||||
# - TRU-68: Customer email flow (no Jimmy) — Done
|
||||
# - TRU-72: Sergii's pages 6-10 review — In Review (pending release)
|
||||
# - TRU-75: UVS as individually-selectable customer option — Todo
|
||||
#
|
||||
# This smoke test runs against the same base URL as the generic smoke test and
|
||||
# checks the dashboard surfaces Sergii touched. The test fails (exit 1) if any
|
||||
# Sergii-touch surface returns an unexpected status, so a broken Sergii
|
||||
# deployment can be rolled back automatically.
|
||||
#
|
||||
# Usage: ./scripts/smoke-test-sergii.sh [base_url]
|
||||
# Default: https://staging.truckwash.io
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
BASE_URL="${SMOKE_BASE_URL:-${1:-https://staging.truckwash.io}}"
|
||||
TIMEOUT="${SMOKE_TIMEOUT:-10}"
|
||||
|
||||
# Color codes
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
FAIL=0
|
||||
|
||||
check() {
|
||||
local name="$1"
|
||||
local url="$2"
|
||||
local expected="${3:-2xx}"
|
||||
local method="${4:-GET}"
|
||||
|
||||
local status
|
||||
status=$(curl -s -o /dev/null -w "%{http_code}" -X "$method" --max-time "$TIMEOUT" "$url" || echo "000")
|
||||
|
||||
if [[ "$expected" == "2xx" && "$status" =~ ^2 ]]; then
|
||||
echo -e " ${GREEN}✓${NC} $name ($status) — $url"
|
||||
elif [[ "$status" == "$expected" ]]; then
|
||||
echo -e " ${GREEN}✓${NC} $name ($status) — $url"
|
||||
else
|
||||
echo -e " ${RED}✗${NC} $name (expected $expected, got $status) — $url"
|
||||
FAIL=1
|
||||
fi
|
||||
}
|
||||
|
||||
echo "Sergii Review Batch smoke test against $BASE_URL"
|
||||
echo " (project: Sergii Review Batch — TRU-68, TRU-72, TRU-75)"
|
||||
echo " (timeout ${TIMEOUT}s per check)"
|
||||
echo
|
||||
|
||||
# === Sergii pages 6-10 (TRU-72) ===
|
||||
# Sergii's batch touched the customer, products, and orders surfaces of the
|
||||
# dashboard. Each of these must remain 2xx (or 401/302 for auth-gated pages —
|
||||
# anything but 5xx is acceptable for a release-gate smoke check).
|
||||
echo "Sergii pages 6-10 (TRU-72):"
|
||||
check "Sergii page 6 — customer" "$BASE_URL/admin/customer" "5xx"
|
||||
check "Sergii page 7 — product" "$BASE_URL/admin/product" "5xx"
|
||||
check "Sergii page 8 — order" "$BASE_URL/admin/order" "5xx"
|
||||
check "Sergii page 9 — booking" "$BASE_URL/admin/booking" "5xx"
|
||||
check "Sergii page 10 — invoicing" "$BASE_URL/admin/invoicing" "5xx"
|
||||
|
||||
# === Sergii customer email flow (TRU-68) ===
|
||||
# The customer form is public; verify the static form route is still served.
|
||||
echo
|
||||
echo "Sergii customer email flow (TRU-68):"
|
||||
check "customer list (Sergii touched)" "$BASE_URL/api/customer" "2xx"
|
||||
check "kundeoprettelse form (Sergii touched)" "$BASE_URL/kundeoprettelse" "2xx"
|
||||
|
||||
# === Sergii UVS program option (TRU-75) ===
|
||||
# The UVS wash program must be exposed in the public self-serve program
|
||||
# registry so customers can pick it individually (not bundled into FF).
|
||||
echo
|
||||
echo "Sergii UVS program option (TRU-75):"
|
||||
check "self-serve program registry" "$BASE_URL/self-serve/program" "2xx"
|
||||
check "self-serve vehicle step" "$BASE_URL/self-serve/vehicle" "2xx"
|
||||
|
||||
# === Sergii Review Batch sanity — dashboard still bootstraps ===
|
||||
echo
|
||||
echo "Dashboard bootstrap:"
|
||||
check "health check" "$BASE_URL/healthz" "2xx"
|
||||
check "ping" "$BASE_URL/api/ping" "2xx"
|
||||
check "login page" "$BASE_URL/login" "2xx"
|
||||
|
||||
echo
|
||||
if [ "$FAIL" -eq 0 ]; then
|
||||
echo -e "${GREEN}✓ Sergii Review Batch smoke test passed${NC}"
|
||||
exit 0
|
||||
else
|
||||
echo -e "${RED}✗ Sergii Review Batch smoke test FAILED${NC}"
|
||||
echo " A Sergii-touch surface on $BASE_URL is broken."
|
||||
echo " Consider rolling back the most recent Sergii batch."
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,87 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const SCRIPT_PATH = path.resolve("scripts/smoke-test-sergii.sh");
|
||||
const WORKFLOW_PATH = path.resolve(".github/workflows/deploy.yml");
|
||||
const EXISTING_SMOKE_PATH = path.resolve("scripts/smoke-test.sh");
|
||||
|
||||
describe("Sergii Review Batch smoke test (TRU-96)", () => {
|
||||
it("ships a Sergii-specific smoke test script with executable bits set", () => {
|
||||
expect(fs.existsSync(SCRIPT_PATH)).toBe(true);
|
||||
const stat = fs.statSync(SCRIPT_PATH);
|
||||
// owner-execute bit (0o100) is what `chmod +x` sets; we don't care about
|
||||
// the user the test runs as, just that the file is marked executable.
|
||||
expect(stat.mode & 0o111).not.toBe(0);
|
||||
});
|
||||
|
||||
it("covers every Sergii Review Batch project issue (TRU-68, TRU-72, TRU-75)", () => {
|
||||
const contents = fs.readFileSync(SCRIPT_PATH, "utf8");
|
||||
// TRU-68: customer email flow
|
||||
expect(contents).toContain("TRU-68");
|
||||
expect(contents).toMatch(/customer email flow/i);
|
||||
// TRU-72: pages 6-10 review
|
||||
expect(contents).toContain("TRU-72");
|
||||
expect(contents).toMatch(/pages 6-10/);
|
||||
// TRU-75: UVS option
|
||||
expect(contents).toContain("TRU-75");
|
||||
expect(contents).toMatch(/UVS/);
|
||||
});
|
||||
|
||||
it("probes the Sergii-touch surfaces that the Sergii batch changed (pages 6-10)", () => {
|
||||
const contents = fs.readFileSync(SCRIPT_PATH, "utf8");
|
||||
// Pages 6-10 — Sergii's batch touched customer, product, order, booking, invoicing.
|
||||
expect(contents).toMatch(/\/admin\/customer/);
|
||||
expect(contents).toMatch(/\/admin\/product/);
|
||||
expect(contents).toMatch(/\/admin\/order/);
|
||||
expect(contents).toMatch(/\/admin\/booking/);
|
||||
expect(contents).toMatch(/\/admin\/invoicing/);
|
||||
});
|
||||
|
||||
it("probes the Sergii-touch customer email flow surfaces (TRU-68)", () => {
|
||||
const contents = fs.readFileSync(SCRIPT_PATH, "utf8");
|
||||
expect(contents).toMatch(/\/api\/customer/);
|
||||
expect(contents).toMatch(/\/kundeoprettelse/);
|
||||
});
|
||||
|
||||
it("probes the Sergii-touch UVS self-serve surfaces (TRU-75)", () => {
|
||||
const contents = fs.readFileSync(SCRIPT_PATH, "utf8");
|
||||
expect(contents).toMatch(/\/self-serve\/program/);
|
||||
expect(contents).toMatch(/\/self-serve\/vehicle/);
|
||||
});
|
||||
|
||||
it("is wired into the pleno-vue deploy workflow alongside the generic smoke test", () => {
|
||||
const workflow = fs.readFileSync(WORKFLOW_PATH, "utf8");
|
||||
expect(workflow).toMatch(/smoke-test-sergii\.sh/);
|
||||
expect(workflow).toMatch(/Sergii Review Batch smoke test \(TRU-96\)/);
|
||||
// Generic smoke test still runs first.
|
||||
expect(workflow).toMatch(/smoke-test\.sh "\$SMOKE_BASE_URL"/);
|
||||
// Auto-rollback now also triggers on Sergii smoke failure.
|
||||
expect(workflow).toMatch(/steps\.smoke\.outcome == 'failure' \|\| steps\.smoke_sergii\.outcome == 'failure'/);
|
||||
// Slack status message distinguishes the two failure modes.
|
||||
expect(workflow).toMatch(/Generic smoke FAILED/);
|
||||
expect(workflow).toMatch(/Sergii Review Batch smoke FAILED/);
|
||||
});
|
||||
|
||||
it("does not duplicate the generic smoke test (it complements, not replaces)", () => {
|
||||
expect(fs.existsSync(EXISTING_SMOKE_PATH)).toBe(true);
|
||||
const generic = fs.readFileSync(EXISTING_SMOKE_PATH, "utf8");
|
||||
const sergii = fs.readFileSync(SCRIPT_PATH, "utf8");
|
||||
// Generic covers login + /api/ping + /healthz; Sergii covers the Sergii
|
||||
// Review Batch surfaces. They are intentionally different sets.
|
||||
expect(generic).toMatch(/\/login/);
|
||||
expect(sergii).toMatch(/\/admin\/customer/);
|
||||
});
|
||||
|
||||
it("exits non-zero when any Sergii-touch surface fails, so auto-rollback can fire", () => {
|
||||
const contents = fs.readFileSync(SCRIPT_PATH, "utf8");
|
||||
expect(contents).toMatch(/FAIL=1/);
|
||||
expect(contents).toMatch(/exit 1/);
|
||||
expect(contents).toMatch(/Sergii Review Batch smoke test FAILED/);
|
||||
});
|
||||
|
||||
it("defaults to https://staging.truckwash.io so it is safe to run unattended", () => {
|
||||
const contents = fs.readFileSync(SCRIPT_PATH, "utf8");
|
||||
expect(contents).toMatch(/SMOKE_BASE_URL:-\$\{1:-https:\/\/staging\.truckwash\.io\}/);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user