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:
Jeppe B
2026-08-16 22:06:06 +02:00
committed by GitHub
co-authored by Pleno Bugfix Bot Truck Wash Bugfix
parent 6f39eb897c
commit 2401ebb674
3 changed files with 200 additions and 4 deletions
+13 -4
View File
@@ -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 }}
+100
View File
@@ -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
+87
View File
@@ -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\}/);
});
});