Files
2026-08-21 17:25:27 +05:30

314 lines
19 KiB
Bash
Executable File

#!/usr/bin/env bash
# Central-dashboard API smoke suite.
#
# Creates a throwaway client/project with synthetic equator GPS videos, exercises
# road-types, chainage (quick assign in driving direction, client-frame sr,
# recompute, contamination detector, saved summary), worker claim/push with
# frame-exact geo stamping, GPS correction (viterbi + missing-start stitch with
# the duration guard), permissions, and cleans everything up.
#
# Run from anywhere on the docker host: bash central/scripts/smoke.sh
# Requires: the compose stack up, python3, curl.
set -u
cd "$(dirname "$0")/.."
PASS=0; FAIL=0
ck() { if [ "${2:-1}" -eq 0 ]; then PASS=$((PASS+1)); echo " ok - $1"; else FAIL=$((FAIL+1)); echo " FAIL- $1"; fi; }
PORT=$(docker port central-server-1 8080/tcp 2>/dev/null | head -1 | awk -F: '{print $NF}')
API="http://localhost:${PORT:-8091}/api"
TOK=${ADMIN_TOKEN:-198eb78d0687cca88e22f76759f0154bf85c3f926f225c6c7c84538a57c25ef7}
T="Authorization: Bearer $TOK"
# -q so INSERT/UPDATE command tags ("INSERT 0 1") don't pollute captured values;
# tr -d '\r' because docker exec adds CRs that break arithmetic/URL interpolation.
PSQL() { docker compose exec -T db psql -U central -d central -qtAc "$1" | tr -d '\r'; }
J() { python3 -c "import json,sys; d=json.load(open('$1')); print(d$2)"; }
echo "== setup: throwaway client/project/worker =="
curl -sf -X POST -H "$T" -H 'Content-Type: application/json' -d '{"name":"smoke-client"}' \
$API/clients > /tmp/cl.json; ck "create client" $?
CID=$(J /tmp/cl.json "['id']")
curl -sf -X POST -H "$T" -H 'Content-Type: application/json' \
-d "{\"client_id\":$CID,\"name\":\"smoke-project\",\"source_path\":\"/tmp\",\"source_kind\":\"local\"}" \
$API/projects > /tmp/pr.json; ck "create project" $?
PID=$(J /tmp/pr.json "['id']")
curl -sf -X POST -H "$T" -H 'Content-Type: application/json' \
-d '{"username":"smoke_worker","display_name":"Smoke","role":"ml_support"}' \
$API/users > /tmp/wk.json; ck "create worker" $?
WTOK=$(J /tmp/wk.json "['token']")
WT="Authorization: Bearer $WTOK"
# Two 1113 m tracks along the equator: v1 west→east (lon 0→0.01), v2 filmed the
# OTHER way (lon 0.02→0.01) so the pair exercises flip handling. 1 Hz, 50 pts.
python3 - <<'EOF'
import json
t1 = [[0.0, i * 0.01 / 49, float(i)] for i in range(50)]
t2 = [[0.0, 0.02 - i * 0.01 / 49, float(i)] for i in range(50)]
open('/tmp/t1.json', 'w').write(json.dumps(t1))
open('/tmp/t2.json', 'w').write(json.dumps(t2))
EOF
V1=$(PSQL "INSERT INTO videos (project_id, file_name, rel_path, status) VALUES ($PID,'smoke_v1.mp4','smoke_v1.mp4','pending') RETURNING id")
V2=$(PSQL "INSERT INTO videos (project_id, file_name, rel_path, status) VALUES ($PID,'smoke_v2.mp4','smoke_v2.mp4','pending') RETURNING id")
# v1 also carries an ingested export doc, so the claim path has something to stamp.
DOC='{"video":{"file_name":"smoke_v1.mp4","fps":30},"fixed_annotations":[{"label":"seed","frame_number":0,"vertices":[],"annotated_by":"ingest"}],"range_annotations":[]}'
PSQL "UPDATE videos SET gps_status='ok', gps_track='$(cat /tmp/t1.json)'::jsonb, gps_len_m=1113, fps=30, frame_count=1500, width=1920, height=1080, duration_s=50, status='annotated', annotation_count=1, raw_json='$DOC'::jsonb WHERE id=$V1" >/dev/null
PSQL "UPDATE videos SET gps_status='ok', gps_track='$(cat /tmp/t2.json)'::jsonb, gps_len_m=1113, fps=30, frame_count=1500, width=1920, height=1080, duration_s=50 WHERE id=$V2" >/dev/null
[ -n "$V1" ] && [ -n "$V2" ]; ck "seed synthetic GPS videos ($V1, $V2)" $?
echo "== road types =="
curl -sf -X POST -H "$T" -H 'Content-Type: application/json' -d '{"names":["LHS"]}' \
$API/projects/$PID/road_types >/dev/null; ck "set road types" $?
curl -sf -X POST -H "$T" -H 'Content-Type: application/json' -d "{\"video_ids\":[$V1,$V2],\"road_type\":\"LHS\"}" \
$API/projects/$PID/road_type_assign | grep -q '"assigned":2'; ck "assign LHS" $?
curl -sf -X POST -H "$T" -H 'Content-Type: application/json' -d "{\"video_ids\":[$V1],\"road_type\":\"\"}" \
$API/projects/$PID/road_type_assign >/dev/null; ck "clear road type ('')" $?
curl -sf -X POST -H "$T" -H 'Content-Type: application/json' -d "{\"video_ids\":[$V1],\"road_type\":\"LHS\"}" \
$API/projects/$PID/road_type_assign >/dev/null
echo "== chainage: quick assign (driving 0 -> 2000 m over a ~2226 m route) =="
curl -sf -X POST -H "$T" -H 'Content-Type: application/json' \
-d "{\"video_ids\":[$V1,$V2],\"start_m\":0,\"end_m\":2000,\"road_type\":\"LHS\"}" \
$API/projects/$PID/chainage/quick > /tmp/quick.json; ck "quick assign runs" $?
python3 - "$V1" "$V2" <<'EOF'
import json, sys
d = json.load(open("/tmp/quick.json"))
assert 2100 < d["route_len_m"] < 2400, d["route_len_m"]
vids = {v["id"]: v for v in d["videos"]}
a, b = vids[int(sys.argv[1])], vids[int(sys.argv[2])]
ends = sorted([a["chainage_start_m"], a["chainage_end_m"], b["chainage_start_m"], b["chainage_end_m"]])
for got, want in zip(ends, [0, 1000, 1000, 2000]):
assert abs(got - want) < 60, ends
assert b["flipped"] != a["flipped"]
scale = d["spans"][0]["scale"]
assert abs(scale - 2000 / d["route_len_m"]) < 0.01, scale
print(f" route={d['route_len_m']:.0f}m scale=x{scale:.4f}")
EOF
ck "two-point error distribution + flip detection" $?
echo "== chainage: DECREASING trend (driving 5000 -> 3000) + client-frame sr =="
curl -sf -X POST -H "$T" -H 'Content-Type: application/json' \
-d "{\"video_ids\":[$V1,$V2],\"start_m\":5000,\"end_m\":3000,\"road_type\":\"LHS\"}" \
$API/projects/$PID/chainage/quick > /tmp/quickd.json; ck "decreasing quick assign runs" $?
python3 <<'EOF'
import json
d = json.load(open("/tmp/quickd.json"))
L = d["route_len_m"]
ch = [x for v in d["videos"] for x in (v["chainage_start_m"], v["chainage_end_m"])]
sr = [x for v in d["videos"] for x in (v["sr_start_m"], v["sr_end_m"])]
# Client chainage spans 3000..5000 in the entered trend.
assert abs(min(ch) - 3000) < 60 and abs(max(ch) - 5000) < 60, (min(ch), max(ch))
# sr is measured meters ON THE CLIENT'S AXIS: anchored at 3000, spanning +route_len
# (not a bare 0..L axis).
assert abs(min(sr) - 3000) < 60, min(sr)
assert abs(max(sr) - (3000 + L)) < 60, (max(sr), L)
# Per video, sr and chainage always run the SAME direction.
signs = set()
for v in d["videos"]:
cd = v["chainage_end_m"] - v["chainage_start_m"]
sd = v["sr_end_m"] - v["sr_start_m"]
assert cd * sd > 0, v
signs.add(cd > 0)
assert signs == {True, False}, "one of the two opposing videos must decrease"
print(f" ch [{min(ch):.0f}..{max(ch):.0f}] sr [{min(sr):.0f}..{max(sr):.0f}] (L={L:.0f})")
EOF
ck "decreasing chainage + sr in the client frame" $?
# Restore the 0 -> 2000 calibration for the tests below.
curl -sf -X POST -H "$T" -H 'Content-Type: application/json' \
-d "{\"video_ids\":[$V1,$V2],\"start_m\":0,\"end_m\":2000,\"road_type\":\"LHS\"}" \
$API/projects/$PID/chainage/quick >/dev/null; ck "restore increasing calibration" $?
echo "== chainage: midpoint + far-point exclusion =="
curl -sf -X POST -H "$T" -H 'Content-Type: application/json' \
-d '{"lat":0.0,"lon":0.01,"chainage_m":900,"note":"client mid","road_type":"LHS"}' \
$API/projects/$PID/chainage/points >/dev/null; ck "add midpoint 0.9 km" $?
curl -sf -X POST -H "$T" -H 'Content-Type: application/json' \
-d '{"lat":1.0,"lon":0.01,"chainage_m":500,"note":"bad","road_type":"LHS"}' \
$API/projects/$PID/chainage/points >/dev/null
curl -sf -X POST -H "$T" -H 'Content-Type: application/json' \
-d "{\"video_ids\":[$V1,$V2],\"road_type\":\"LHS\"}" \
$API/projects/$PID/chainage/recompute > /tmp/rec.json; ck "recompute runs" $?
python3 - "$V1" "$V2" <<'EOF'
import json, sys
d = json.load(open("/tmp/rec.json"))
assert d["points_used"] == 3 and d["points_excluded"] == 1, (d["points_used"], d["points_excluded"])
vids = {v["id"]: v for v in d["videos"]}
a, b = vids[int(sys.argv[1])], vids[int(sys.argv[2])]
ja = min(abs(a["chainage_start_m"] - 900), abs(a["chainage_end_m"] - 900))
jb = min(abs(b["chainage_start_m"] - 900), abs(b["chainage_end_m"] - 900))
assert ja < 40 and jb < 40, (ja, jb)
scales = sorted(sp["scale"] for sp in d["spans"])
assert abs(scales[0] - 900 / 1113) < 0.03 and abs(scales[1] - 1100 / 1113) < 0.03, scales
print(f" spans x{scales[0]:.3f}/x{scales[1]:.3f}, junction pinned at 900 m")
EOF
ck "per-span distribution + snap guard" $?
CAL=$(PSQL "SELECT count(*) FROM chainage_calibrations WHERE project_id=$PID")
[ "$CAL" -ge 1 ]; ck "calibration persisted" $?
echo "== against-flow, calibrations list, quick notes, note edit =="
python3 <<'EOF'
import json
d = json.load(open("/tmp/rec.json"))
assert "majority_flipped" in d and d["majority_flipped"] is False
for v in d["videos"]:
assert v["against_flow"] == v["flipped"], v
EOF
ck "against_flow relative to majority" $?
curl -sf -H "$T" $API/projects/$PID/chainage/calibrations > /tmp/cals.json; ck "calibrations list (GET)" $?
python3 -c "
import json; c=[x for x in json.load(open('/tmp/cals.json')) if x['road_type']=='LHS'][0]
assert c['anchor_count'] >= 2 and c['computed_by'] and c['route_len_m'] > 2000 and c['has_summary'], c"; ck "calibration row (anchors, author, summary)" $?
curl -sf -H "$T" $API/projects/$PID/chainage/points > /tmp/pts.json
python3 -c "
import json; n=[p['note'] for p in json.load(open('/tmp/pts.json'))]
assert 'quick: drive start' in n and 'quick: drive end' in n, n"; ck "quick points labeled drive start/end" $?
QP=$(python3 -c "import json; print([p['id'] for p in json.load(open('/tmp/pts.json')) if p['origin']=='quick'][0])")
curl -sf -X POST -H "$T" -H 'Content-Type: application/json' -d '{"note":"toll plaza"}' \
$API/chainage/points/$QP | grep -q '"updated":true'; ck "point note edit" $?
curl -sf -H "$T" $API/projects/$PID/chainage/points | grep -q '"toll plaza"'; ck "edited note persisted" $?
curl -sf -X POST -H "$T" -H 'Content-Type: application/json' \
-d "{\"video_ids\":[$V1,$V2],\"start_m\":0,\"end_m\":2000,\"road_type\":\"LHS\"}" \
$API/projects/$PID/chainage/quick >/dev/null; ck "quick re-run after rename" $?
NQ=$(PSQL "SELECT count(*) FROM chainage_points WHERE project_id=$PID AND origin='quick'")
[ "$NQ" = "2" ]; ck "renamed quick point replaced (origin identity, 2 quick points)" $?
echo "== contamination detector (untagged point near two groups) =="
curl -sf -X POST -H "$T" -H 'Content-Type: application/json' -d '{"names":["LHS","RHS"]}' \
$API/projects/$PID/road_types >/dev/null
curl -sf -X POST -H "$T" -H 'Content-Type: application/json' -d "{\"video_ids\":[$V2],\"road_type\":\"RHS\"}" \
$API/projects/$PID/road_type_assign >/dev/null; ck "move v2 to RHS" $?
curl -sf -X POST -H "$T" -H 'Content-Type: application/json' \
-d '{"lat":0.0,"lon":0.0099,"chainage_m":1100,"note":"junction","road_type":""}' \
$API/projects/$PID/chainage/points > /tmp/ambig.json; ck "add untagged junction point" $?
AMB=$(J /tmp/ambig.json "['id']")
curl -sf -X POST -H "$T" -H 'Content-Type: application/json' -d "{\"video_ids\":[$V1],\"road_type\":\"LHS\"}" \
$API/projects/$PID/chainage/recompute > /tmp/rec2.json; ck "LHS-only recompute" $?
python3 -c "
import json; w=json.load(open('/tmp/rec2.json'))['warnings']
assert any('ambiguous' in x and 'RHS' in x for x in w), w"; ck "untagged point near RHS flagged ambiguous" $?
curl -sf -X DELETE -H "$T" $API/chainage/points/$AMB >/dev/null
curl -sf -X POST -H "$T" -H 'Content-Type: application/json' -d "{\"video_ids\":[$V2],\"road_type\":\"LHS\"}" \
$API/projects/$PID/road_type_assign >/dev/null
curl -sf -X POST -H "$T" -H 'Content-Type: application/json' -d "{\"video_ids\":[$V1,$V2],\"road_type\":\"LHS\"}" \
$API/projects/$PID/chainage/recompute > /tmp/rec3.json; ck "state restored (full recompute)" $?
curl -sf -H "$T" "$API/projects/$PID/chainage/summary?road_type=LHS" > /tmp/saved.json; ck "saved summary (GET)" $?
python3 -c "
import json
a = json.load(open('/tmp/rec3.json')); b = json.load(open('/tmp/saved.json'))
assert b is not None and abs(a['route_len_m'] - b['route_len_m']) < 0.01
assert len(b['videos']) == 2 and 'majority_flipped' in b, list(b)"; ck "saved summary matches the compute" $?
echo "== wire format: claim + push + frame-exact geo =="
curl -sf -X POST -H "$T" $API/videos/$V1/claim > /tmp/claim.json; ck "claim" $?
python3 -c "
import json; a=json.load(open('/tmp/claim.json'))['annotations']
assert a['road_type']=='LHS' and a['chainage_start'] is not None, a.get('road_type')"; ck "claim doc stamped" $?
curl -sf -X POST -H "$T" -H 'Content-Type: application/json' \
-d '{"video":{"file_name":"smoke_v1.mp4","fps":30},"fixed_annotations":[{"label":"pothole","frame_number":15,"vertices":[],"annotated_by":"smoke"}],"range_annotations":[{"label":"crack","start_frame":0,"end_frame":30,"start_vertices":[],"annotated_by":"smoke"}],"verify_time_ms":1000}' \
$API/videos/$V1/push >/dev/null; ck "push" $?
curl -sf -H "$T" $API/projects/$PID/videos > /tmp/vids.json
PSQL "SELECT raw_json->'fixed_annotations'->0 FROM videos WHERE id=$V1" > /tmp/ann0.json
PSQL "SELECT raw_json->'range_annotations'->0 FROM videos WHERE id=$V1" > /tmp/rann0.json
python3 - "$V1" <<'EOF'
import json, sys
v = {x['id']: x for x in json.load(open('/tmp/vids.json'))}[int(sys.argv[1])]
a = json.load(open('/tmp/ann0.json'))
span = v['chainage_end_m'] - v['chainage_start_m']
exp_km = (v['chainage_start_m'] + span * (11.36 / 1113.2)) / 1000.0
assert a['chainage'] is not None and abs(a['chainage'] - exp_km) < 0.0016, (a['chainage'], exp_km)
assert a['chainage'] == round(a['chainage'], 4), a['chainage'] # km, 4 decimals
assert 'chainage_m' not in a, 'old meter key must be gone'
assert abs(a['lat']) < 1e-5 and abs(a['lon'] - 0.000102) < 2e-5, (a['lat'], a['lon'])
# sr runs the same direction as chainage for this annotation's video.
sd = v['sr_chainage_end_m'] - v['sr_chainage_start_m']
assert sd * span > 0, (sd, span)
r = json.load(open('/tmp/rann0.json'))
assert abs(r['chainage'] - v['chainage_start_m'] / 1000.0) < 0.001 and r['end_chainage'] is not None
print(f" ann@frame15 chainage={a['chainage']} km (exp {exp_km:.4f})")
EOF
ck "annotations stamped at their exact frame" $?
RJ=$(PSQL "SELECT raw_json->>'road_type' FROM videos WHERE id=$V1")
[ "$RJ" = "LHS" ]; ck "push re-stamped road_type ('$RJ')" $?
curl -sf "$API/videos/$V1/export?token=$TOK" | python3 -c "
import json,sys; d=json.load(sys.stdin); assert d['road_type']=='LHS' and d['chainage_start'] is not None"; ck "export stamped" $?
echo "== from-scratch annotate: raw video (NULL fps) gets geo on push =="
PSQL "UPDATE videos SET fps=NULL, frame_count=NULL WHERE id=$V2" >/dev/null
curl -sf -X POST -H "$T" $API/videos/$V2/claim >/dev/null; ck "claim raw video" $?
curl -sf -X POST -H "$T" -H 'Content-Type: application/json' \
-d '{"video":{"file_name":"smoke_v2.mp4","fps":30,"frame_count":1500,"width":1920,"height":1080},"fixed_annotations":[{"label":"pothole","frame_number":30,"vertices":[],"annotated_by":"smoke"}],"range_annotations":[],"verify_time_ms":500}' \
$API/videos/$V2/push >/dev/null; ck "push from-scratch annotation" $?
FPS=$(PSQL "SELECT fps FROM videos WHERE id=$V2")
[ "$FPS" = "30" ]; ck "server adopted doc fps ($FPS)" $?
PSQL "SELECT raw_json->'fixed_annotations'->0->>'chainage' FROM videos WHERE id=$V2" | grep -q '[0-9]'; ck "new annotation got geo" $?
echo "== KML tracks export =="
curl -sf "$API/projects/$PID/tracks.kml?token=$TOK" > /tmp/tracks.kml; ck "tracks.kml downloads" $?
grep -q "LineString" /tmp/tracks.kml && grep -q "Placemark" /tmp/tracks.kml; ck "KML has LineStrings + points" $?
echo "== viterbi correction (inject 1.1 km outlier into v1) =="
python3 -c "
import json
t = json.load(open('/tmp/t1.json')); t[30][0] = 0.01
open('/tmp/t1_bad.json','w').write(json.dumps(t))"
PSQL "UPDATE videos SET gps_track='$(cat /tmp/t1_bad.json)'::jsonb, gps_track_corrected=NULL, gps_corrected_by=NULL WHERE id=$V1" >/dev/null
curl -sf -X POST -H "$T" -H 'Content-Type: application/json' -d '{"action":"preview"}' \
$API/videos/$V1/gps/correct > /tmp/prev.json; ck "preview" $?
python3 -c "
import json; d=json.load(open('/tmp/prev.json'))
assert d['replaced'] >= 1 and d['len_after_m'] < d['len_before_m'], (d['replaced'], d['len_before_m'], d['len_after_m'])"; ck "outlier removed" $?
curl -sf -X POST -H "$T" -H 'Content-Type: application/json' -d '{"action":"keep"}' \
$API/videos/$V1/gps/correct >/dev/null; ck "keep" $?
RAW=$(PSQL "SELECT gps_track->30->>0 FROM videos WHERE id=$V1")
[ "$RAW" = "0.01" ]; ck "raw immutable ($RAW)" $?
curl -sf -X POST -H "$T" -H 'Content-Type: application/json' -d '{"action":"discard"}' \
$API/videos/$V1/gps/correct >/dev/null
CORR=$(PSQL "SELECT count(*) FROM videos WHERE id=$V1 AND gps_track_corrected IS NOT NULL")
[ "$CORR" = "0" ]; ck "discard reverts to raw" $?
echo "== missing-start stitch + duration guard =="
python3 - <<'EOF'
import json
pred = [[0.0, 0.03 + i * 0.01 / 49, float(i)] for i in range(50)]
succ = [[0.0, 0.0405 + i * 0.0085 / 40, float(i)] for i in range(41)] # span 40 s
open('/tmp/tp.json', 'w').write(json.dumps(pred))
open('/tmp/ts.json', 'w').write(json.dumps(succ))
EOF
V3=$(PSQL "INSERT INTO videos (project_id, file_name, rel_path, status) VALUES ($PID,'smoke_pred.mp4','smoke_pred.mp4','pending') RETURNING id")
V4=$(PSQL "INSERT INTO videos (project_id, file_name, rel_path, status) VALUES ($PID,'smoke_succ.mp4','smoke_succ.mp4','pending') RETURNING id")
V5=$(PSQL "INSERT INTO videos (project_id, file_name, rel_path, status) VALUES ($PID,'smoke_full.mp4','smoke_full.mp4','pending') RETURNING id")
PSQL "UPDATE videos SET gps_status='ok', gps_track='$(cat /tmp/tp.json)'::jsonb, gps_len_m=1113, fps=30, duration_s=50, gps_start_epoch=1000000000, gps_end_epoch=1000000049 WHERE id=$V3" >/dev/null
PSQL "UPDATE videos SET gps_status='ok', gps_track='$(cat /tmp/ts.json)'::jsonb, gps_len_m=946, fps=30, duration_s=50, gps_start_epoch=1000000058, gps_end_epoch=1000000098 WHERE id=$V4" >/dev/null
PSQL "UPDATE videos SET gps_status='ok', gps_track='$(cat /tmp/ts.json)'::jsonb, gps_len_m=946, fps=30, duration_s=40, gps_start_epoch=1000000058, gps_end_epoch=1000000098 WHERE id=$V5" >/dev/null
curl -sf -X POST -H "$T" -H 'Content-Type: application/json' -d '{"action":"preview"}' \
$API/videos/$V4/gps/correct > /tmp/st.json; ck "stitch preview" $?
python3 -c "
import json; d=json.load(open('/tmp/st.json'))
assert d['stitched_from'], d
assert 2 <= d['shift_s'] <= 20, d['shift_s']
assert abs(d['corrected'][0][2]) < 1e-6, d['corrected'][0]"; ck "start borrowed + clock re-based" $?
curl -sf -X POST -H "$T" -H 'Content-Type: application/json' -d '{"action":"preview"}' \
$API/videos/$V5/gps/correct > /tmp/st2.json; ck "guard preview" $?
python3 -c "
import json; d=json.load(open('/tmp/st2.json'))
assert not d['stitched_from'], d['stitched_from']"; ck "duration guard: complete track untouched" $?
echo "== rescan + permissions =="
curl -sf -X POST -H "$T" $API/videos/$V1/gps/rescan | grep -q '"queued"'; ck "rescan queues" $?
CODE=$(curl -s -o /dev/null -w '%{http_code}' -X POST -H "$WT" -H 'Content-Type: application/json' \
-d '{"names":["X"]}' $API/projects/$PID/road_types)
[ "$CODE" = "403" ]; ck "ml_support cannot edit road types ($CODE)" $?
CODE=$(curl -s -o /dev/null -w '%{http_code}' -X POST -H "$WT" -H 'Content-Type: application/json' \
-d '{"action":"keep"}' $API/videos/$V2/gps/correct)
[ "$CODE" = "403" ]; ck "ml_support cannot keep corrections ($CODE)" $?
echo "== cleanup =="
curl -sf -X DELETE -H "$T" $API/projects/$PID | grep -q deleted; ck "delete project" $?
curl -sf -X DELETE -H "$T" $API/clients/$CID | grep -q deleted; ck "delete client" $?
WID=$(curl -sf -H "$T" $API/users | python3 -c "
import json,sys; print([u['id'] for u in json.load(sys.stdin) if u['username']=='smoke_worker'][0])")
curl -sf -X DELETE -H "$T" $API/users/$WID | grep -q deleted; ck "delete worker" $?
echo
echo "RESULT: $PASS passed, $FAIL failed"
[ "$FAIL" -eq 0 ]