|
1 |
+ |
#!/usr/bin/env bash
|
|
2 |
+ |
# Fail loudly when the testnot.work demo goes thin.
|
|
3 |
+ |
#
|
|
4 |
+ |
# Phase 5 of wiki `testnot-demo-content-plan`. The failure this exists for:
|
|
5 |
+ |
# nine of eleven items went invisible on testnot and nothing said so for weeks.
|
|
6 |
+ |
# The seed's own tests passed the whole time, because tests/workflows/seed_examples.rs
|
|
7 |
+ |
# runs against InMemoryStorage where media always attaches and no project-level
|
|
8 |
+ |
# paywall hides anything. The failure lived exactly in the gap between the test
|
|
9 |
+ |
# and the box, so more unit tests cannot close it. This runs against the live
|
|
10 |
+ |
# site instead.
|
|
11 |
+ |
#
|
|
12 |
+ |
# It checks thinness, not 500s. Every page here can return a cheerful 200 while
|
|
13 |
+ |
# showing nothing, and that is the state it is looking for.
|
|
14 |
+ |
#
|
|
15 |
+ |
# Run it after every reseed (mnw-testnot-seed.sh) and before any capture run
|
|
16 |
+ |
# that photographs this box. The landing carousel will carry screenshots taken
|
|
17 |
+ |
# from here, so a silent reseed that empties the catalog also silently
|
|
18 |
+ |
# invalidates the marketing images.
|
|
19 |
+ |
#
|
|
20 |
+ |
# ./mnw-testnot-smoke.sh # check testnot.work
|
|
21 |
+ |
# BASE=https://testnot.work ./mnw-testnot-smoke.sh
|
|
22 |
+ |
#
|
|
23 |
+ |
# Exit 0 = the demo is worth showing. Exit 1 = it is not; read the FAIL lines.
|
|
24 |
+ |
set -uo pipefail
|
|
25 |
+ |
|
|
26 |
+ |
BASE="${BASE:-https://testnot.work}"
|
|
27 |
+ |
|
|
28 |
+ |
# ── Sealed baselines ────────────────────────────────────────────────────────
|
|
29 |
+ |
#
|
|
30 |
+ |
# Same idea as tests/test_hygiene.rs: freeze what is true today and fail on a
|
|
31 |
+ |
# new violation, rather than demanding a clean sheet nobody will ever deliver.
|
|
32 |
+ |
# A number here may only move in the improving direction, and when it does, edit
|
|
33 |
+ |
# it. Never loosen one to make a run pass.
|
|
34 |
+ |
|
|
35 |
+ |
# Projects the demo must carry. Below this the catalog stops reading as a
|
|
36 |
+ |
# catalog and starts reading as a test fixture.
|
|
37 |
+ |
MIN_PROJECTS=5
|
|
38 |
+ |
|
|
39 |
+ |
# Items a visitor can actually see and click, summed across every storefront
|
|
40 |
+ |
# that is not paywalled. Nine of eleven on 2026-08-06; the other two are behind
|
|
41 |
+ |
# the one deliberate subscription paywall.
|
|
42 |
+ |
MIN_VISIBLE_ITEMS=9
|
|
43 |
+ |
|
|
44 |
+ |
# Covers still served from the seed's generated placeholder (a 16x16 grayscale
|
|
45 |
+ |
# PNG, src/seed/media.rs). Five of five on 2026-08-06: the seed has never
|
|
46 |
+ |
# uploaded real art, and swapping in public-domain media is its own task
|
|
47 |
+ |
# (GoingsOn 223f3c6f, which also gates the landing captures 54730d4d).
|
|
48 |
+ |
#
|
|
49 |
+ |
# This is sealed rather than asserted at zero on purpose. Zero would make the
|
|
50 |
+ |
# script red from its first run, and a check that is always red is a check
|
|
51 |
+ |
# nobody reads. Sealed, it still catches the regression that matters: a project
|
|
52 |
+ |
# whose real cover reverts to a placeholder pushes the count up and fails.
|
|
53 |
+ |
PLACEHOLDER_COVERS_BASELINE=5
|
|
54 |
+ |
|
|
55 |
+ |
# A cover at or under this is the placeholder or a broken upload, not artwork.
|
|
56 |
+ |
# The placeholder is 72 bytes; real cover art is orders of magnitude larger.
|
|
57 |
+ |
REAL_COVER_MIN_BYTES=2048
|
|
58 |
+ |
|
|
59 |
+ |
# A page under this rendered its chrome and nothing else.
|
|
60 |
+ |
MIN_PAGE_BYTES=3000
|
|
61 |
+ |
|
|
62 |
+ |
TMP="$(mktemp -d)"
|
|
63 |
+ |
trap 'rm -rf "$TMP"' EXIT
|
|
64 |
+ |
|
|
65 |
+ |
fails=0
|
|
66 |
+ |
warns=0
|
|
67 |
+ |
pass() { printf ' ok %s\n' "$*"; }
|
|
68 |
+ |
fail() { printf ' FAIL %s\n' "$*"; fails=$((fails + 1)); }
|
|
69 |
+ |
warn() { printf ' warn %s\n' "$*"; warns=$((warns + 1)); }
|
|
70 |
+ |
section() { printf '\n== %s\n' "$*"; }
|
|
71 |
+ |
|
|
72 |
+ |
# Fetch $1 into $TMP/body, echo the status code.
|
|
73 |
+ |
get() {
|
|
74 |
+ |
curl -sS --max-time 30 -o "$TMP/body" -w '%{http_code}' "$BASE$1" 2>/dev/null || echo 000
|
|
75 |
+ |
}
|
|
76 |
+ |
|
|
77 |
+ |
section "reachable"
|
|
78 |
+ |
code=$(get /)
|
|
79 |
+ |
if [ "$code" != "200" ]; then
|
|
80 |
+ |
fail "GET / returned $code; nothing else below is meaningful"
|
|
81 |
+ |
printf '\n%s\n' "smoke: FAILED ($fails)"
|
|
82 |
+ |
exit 1
|
|
83 |
+ |
fi
|
|
84 |
+ |
pass "GET / -> 200"
|
|
85 |
+ |
|
|
86 |
+ |
# ── The catalog ─────────────────────────────────────────────────────────────
|
|
87 |
+ |
#
|
|
88 |
+ |
# Discover is the index and each storefront is the page it points at. Checking
|
|
89 |
+ |
# them against each other is the whole trick: either alone can look healthy
|
|
90 |
+ |
# while disagreeing with the other, which is how the paywall bug hid.
|
|
91 |
+ |
|
|
92 |
+ |
section "catalog"
|
|
93 |
+ |
code=$(get /discover)
|
|
94 |
+ |
[ "$code" = "200" ] || fail "GET /discover returned $code"
|
|
95 |
+ |
|
|
96 |
+ |
# Discover's project rows carry both the slug and the item count it advertises.
|
|
97 |
+ |
grep -oE '<a href="/p/[a-z0-9-]+" class="table-row project-row">|<span class="row-items">[0-9]+</span>' "$TMP/body" |
|
|
98 |
+ |
sed -e 's/.*href="\/p\///' -e 's/" class.*//' -e 's/<span class="row-items">//' -e 's/<\/span>//' |
|
|
99 |
+ |
paste - - >"$TMP/advertised" 2>/dev/null
|
|
100 |
+ |
|
|
101 |
+ |
n_projects=$(wc -l <"$TMP/advertised")
|
|
102 |
+ |
if [ "$n_projects" -lt "$MIN_PROJECTS" ]; then
|
|
103 |
+ |
fail "discover lists $n_projects projects, floor is $MIN_PROJECTS"
|
|
104 |
+ |
else
|
|
105 |
+ |
pass "discover lists $n_projects projects"
|
|
106 |
+ |
fi
|
|
107 |
+ |
|
|
108 |
+ |
visible_total=0
|
|
109 |
+ |
: >"$TMP/covers"
|
|
110 |
+ |
|
|
111 |
+ |
while read -r slug advertised; do
|
|
112 |
+ |
[ -n "$slug" ] || continue
|
|
113 |
+ |
code=$(get "/p/$slug")
|
|
114 |
+ |
if [ "$code" != "200" ]; then
|
|
115 |
+ |
fail "/p/$slug returned $code"
|
|
116 |
+ |
continue
|
|
117 |
+ |
fi
|
|
118 |
+ |
|
|
119 |
+ |
rendered=$(grep -o 'class="item-card' "$TMP/body" | wc -l)
|
|
120 |
+ |
grep -oE 'src="https://[^"]*/image/[^"]*"' "$TMP/body" | sed -e 's/src="//' -e 's/"$//' >>"$TMP/covers"
|
|
121 |
+ |
|
|
122 |
+ |
if grep -q 'class="paywall-box"' "$TMP/body"; then
|
|
123 |
+ |
# A paywalled storefront shows no items by construction: the handler
|
|
124 |
+ |
# returns the paywall template before it reaches them. What it must
|
|
125 |
+ |
# still do is state the count, or it contradicts the discover card that
|
|
126 |
+ |
# sent the visitor here.
|
|
127 |
+ |
if [ "$rendered" -ne 0 ]; then
|
|
128 |
+ |
fail "/p/$slug is paywalled but rendered $rendered item cards"
|
|
129 |
+ |
elif grep -qE "(^|>)[^<]*\b$advertised items? included" "$TMP/body"; then
|
|
130 |
+ |
pass "/p/$slug paywalled, states its $advertised items"
|
|
131 |
+ |
else
|
|
132 |
+ |
fail "/p/$slug is paywalled and does not state its $advertised items; discover advertises them"
|
|
133 |
+ |
fi
|
|
134 |
+ |
else
|
|
135 |
+ |
visible_total=$((visible_total + rendered))
|
|
136 |
+ |
if [ "$rendered" -eq "$advertised" ]; then
|
|
137 |
+ |
pass "/p/$slug renders $rendered items, matching discover"
|
|
138 |
+ |
else
|
|
139 |
+ |
fail "/p/$slug renders $rendered items, discover advertises $advertised"
|
|
140 |
+ |
fi
|
|
141 |
+ |
fi
|
|
142 |
+ |
done <"$TMP/advertised"
|
|
143 |
+ |
|
|
144 |
+ |
if [ "$visible_total" -lt "$MIN_VISIBLE_ITEMS" ]; then
|
|
145 |
+ |
fail "$visible_total items visible across open storefronts, floor is $MIN_VISIBLE_ITEMS"
|
|
146 |
+ |
else
|
|
147 |
+ |
pass "$visible_total items visible across open storefronts"
|
|
148 |
+ |
fi
|
|
149 |
+ |
|
|
150 |
+ |
# ── Covers ──────────────────────────────────────────────────────────────────
|
|
151 |
+ |
#
|
|
152 |
+ |
# A cover URL that 404s still renders as a 200 page with a broken image, which
|
|
153 |
+ |
# no status-code check sees. Fetch each one.
|
|
154 |
+ |
|
|
155 |
+ |
section "covers"
|
|
156 |
+ |
sort -u "$TMP/covers" >"$TMP/covers.uniq"
|
|
157 |
+ |
n_covers=$(wc -l <"$TMP/covers.uniq")
|
|
158 |
+ |
[ "$n_covers" -gt 0 ] || fail "no cover images referenced by any storefront"
|
|
159 |
+ |
|
|
160 |
+ |
placeholders=0
|
|
161 |
+ |
while read -r url; do
|
|
162 |
+ |
[ -n "$url" ] || continue
|
|
163 |
+ |
read -r ccode csize ctype <<<"$(curl -sS --max-time 30 -o /dev/null \
|
|
164 |
+ |
-w '%{http_code} %{size_download} %{content_type}' "$url" 2>/dev/null || echo "000 0 -")"
|
|
165 |
+ |
short="${url##*/projects/}"
|
|
166 |
+ |
case "$ctype" in
|
|
167 |
+ |
image/*) is_image=1 ;;
|
|
168 |
+ |
*) is_image=0 ;;
|
|
169 |
+ |
esac
|
|
170 |
+ |
if [ "$ccode" != "200" ]; then
|
|
171 |
+ |
fail "cover $short returned $ccode"
|
|
172 |
+ |
elif [ "$is_image" -eq 0 ]; then
|
|
173 |
+ |
fail "cover $short served as ${ctype:--}, not an image"
|
|
174 |
+ |
elif [ "$csize" -lt "$REAL_COVER_MIN_BYTES" ]; then
|
|
175 |
+ |
placeholders=$((placeholders + 1))
|
|
176 |
+ |
fi
|
|
177 |
+ |
done <"$TMP/covers.uniq"
|
|
178 |
+ |
|
|
179 |
+ |
if [ "$placeholders" -gt "$PLACEHOLDER_COVERS_BASELINE" ]; then
|
|
180 |
+ |
fail "$placeholders placeholder covers, sealed baseline is $PLACEHOLDER_COVERS_BASELINE (a real cover reverted)"
|
|
181 |
+ |
elif [ "$placeholders" -eq "$PLACEHOLDER_COVERS_BASELINE" ] && [ "$placeholders" -gt 0 ]; then
|
|
182 |
+ |
warn "$placeholders of $n_covers covers are still the seed placeholder (GoingsOn 223f3c6f); at baseline, not a regression"
|
|
183 |
+ |
else
|
|
184 |
+ |
pass "$placeholders placeholder covers, down from a baseline of $PLACEHOLDER_COVERS_BASELINE. Lower PLACEHOLDER_COVERS_BASELINE to $placeholders"
|
|
185 |
+ |
fi
|
|
186 |
+ |
|
|
187 |
+ |
# ── The surfaces that are not the catalog ───────────────────────────────────
|
|
188 |
+ |
#
|
|
189 |
+ |
# Phase 4's list. Each is a place a curious visitor lands, and each can go
|
|
190 |
+ |
# blank without anything else noticing.
|
|
191 |
+ |
|
|
192 |
+ |
section "other surfaces"
|
|
193 |
+ |
for path in /discover?mode=items /pricing /fan-plus /docs; do
|
|
194 |
+ |
code=$(get "$path")
|
|
195 |
+ |
size=$(wc -c <"$TMP/body")
|
|
196 |
+ |
if [ "$code" != "200" ]; then
|
|
197 |
+ |
fail "$path returned $code"
|
|
198 |
+ |
elif [ "$size" -lt "$MIN_PAGE_BYTES" ]; then
|
|
199 |
+ |
fail "$path returned 200 but only ${size}B, which is chrome and no content"
|
|
200 |
+ |
else
|
|
201 |
+ |
pass "$path -> 200, ${size}B"
|
|
202 |
+ |
fi
|
|
203 |
+ |
done
|
|
204 |
+ |
|
|
205 |
+ |
section "blogs and feeds"
|
|
206 |
+ |
while read -r slug _; do
|
|
207 |
+ |
[ -n "$slug" ] || continue
|
|
208 |
+ |
code=$(get "/p/$slug/blog")
|
|
209 |
+ |
[ "$code" = "200" ] || fail "/p/$slug/blog returned $code"
|
|
210 |
+ |
|
|
211 |
+ |
# Note the path: the blog feed is feed.xml, not rss. `/p/{slug}/rss` is a
|
|
212 |
+ |
# different feed, of the project's items.
|
|
213 |
+ |
code=$(get "/p/$slug/blog/feed.xml")
|
|
214 |
+ |
posts=$(grep -c '<item>' "$TMP/body" 2>/dev/null || echo 0)
|
|
215 |
+ |
if [ "$code" != "200" ]; then
|
|
216 |
+ |
fail "/p/$slug/blog/feed.xml returned $code"
|
|
217 |
+ |
elif [ "$posts" -lt 1 ]; then
|
|
218 |
+ |
fail "/p/$slug/blog/feed.xml is an empty feed"
|
|
219 |
+ |
else
|
|
220 |
+ |
pass "/p/$slug blog + feed ($posts posts)"
|
|
221 |
+ |
fi
|
|
222 |
+ |
done <"$TMP/advertised"
|
|
223 |
+ |
|
|
224 |
+ |
# ── Deferred ────────────────────────────────────────────────────────────────
|
|
225 |
+ |
|
|
226 |
+ |
section "deferred"
|
|
227 |
+ |
code=$(get /library)
|
|
228 |
+ |
if [ "$code" = "401" ] || [ "$code" = "302" ] || [ "$code" = "303" ]; then
|
|
229 |
+ |
warn "/library is $code to anonymous, as expected. Checking it needs the seeded demo buyer from GoingsOn 839a8e5a (Phase 3, decided B, not built)"
|
|
230 |
+ |
else
|
|
231 |
+ |
warn "/library returned $code to an anonymous visitor; expected an auth redirect. Worth a look"
|
|
232 |
+ |
fi
|
|
233 |
+ |
|
|
234 |
+ |
printf '\n'
|
|
235 |
+ |
if [ "$fails" -gt 0 ]; then
|
|
236 |
+ |
printf 'smoke: FAILED (%d failures, %d warnings)\n' "$fails" "$warns"
|
|
237 |
+ |
exit 1
|
|
238 |
+ |
fi
|
|
239 |
+ |
printf 'smoke: ok (%d warnings)\n' "$warns"
|