You can boot redhill_godverse, walk down Musgrave Road into Monster Robot Party, riffle its own real crate and buy a real record. Proven falsifiably, not by eyeball: base assets/stock_godverse/3962749/ (its own, not the town-wide) TITLES 120/120 the shop's atlas · 0/120 the generic pack TEXTURES 2 from stock_godverse/3962749/*.webp · 0 generic — the pixels' own file BUY rec_0000 "Abstract Latin Lounge II (Part II)" @ $15 → cash 191→176, exact REBOOT 191/0 restored — gone-this-session, back next boot Ladder: ?noassets → ZERO network requests · keyed-but-atlas-less shop → 404 → parody, live · newtown_real → base null, 350 items: the wire is a NO-OP off the godverse path. flags_check GREEN (0 fails, 0 warns). #6a THE WIRE: godverseBaseFor(shop), exported and used by BOTH the shell's boot preload and the room's lookup — C's cache identity is the LITERAL (type, base) pair, so two sides building that string differently would drop a real shop to parody, a bug indistinguishable from "no stock" by eye. One function, one string, one key. Gated on stockReal, never the id alone. #6b THE GATE: smoke_real_crate + DBG.stockInfo() — permanent, and it cannot pass without touching its subject (SKIPs with a printed reason when the town/atlas is absent). FINDING — THE BRIEF'S OWN HONEST-GATE SPEC WAS VACUOUS. It specified id equality. Both packs number items POSITIONALLY (rec_0000…), so all 120 atlas ids are generic-pack ids too: an id-equality gate goes GREEN on the generic pack — the exact failure the vacuous-gate law was written to stop, reproduced inside its first application. Ids there are slot numbers, not identity. Titles (0 overlap) and the texture URL discriminate; F asserts on those. FINDING — THE ID SPACES COLLIDE, and it bit F's own selector inside the anti-vacuous gate. Plan ids (1..N) and godverse ids (31, 767, 3962749) share a numeric space: Silky Oak's godverse id is 31 and another shop's PLAN id is 31, so enterShop(31) silently entered "Wholefood Cafe" and the soft-fall assertion PASSED ON AN UNKEYED CAFE. Fixed both ends: 'g:<n>' selects a godverse id explicitly, a bare number stays the plan id and now REPORTS the collision, and the smoke asserts it landed in the shop it asked for before asserting anything about it. FINDING — F'S OWN GATE CRIED THE LOUDEST WOLF WE OWN. classic_regression reported "plan fingerprint != golden 0x3fa36874 — determinism/prime-law broken" while the covenant was provably intact. F wrote it in R16 as `hash_ok and returncode == 0` — two assertions, ONE verdict, reported with the fingerprint's message — so any unrelated selfcheck red screamed that the byte-identical-forever covenant had broken. Two assertions, two verdicts now. It isn't enough to fail for a reason; you have to fail for YOUR reason. FINDING — fail-soft-by-404 vs the 0-console-errors law. The boot preload must resolve a keyed shop's pack before the door opens (getStockPack is sync), so it probes blind: 9 of Red Hill's 10 keyed shops have no atlas → 404 → parody. That IS fail-soft working, and it is also 8 console errors no .catch() can suppress. Fix is not to loosen the law but to stop probing for what isn't there: an atlas index in G's namespace (or hasStock on the cache entry, A+G). Filed as a contract ask, not invented. Until then the smoke counts and NAMES the probes and fails on any other error — tolerance stated out loud. Verified the lift is clean: cache 10 keyed → plan 9 keyed, every one a real secondhand shop. No cafe keyed, zero duplicate ids — there is no mis-stocking. F did NOT touch A's identity sweep or E's validator. F was the party that would benefit, and changing a red gate to green in another lane's file to unblock F's own tag would retroactively devalue every green F has ever reported. Filed instead — and R25 has since landed A's reconciliation and the six pins, which is the right hands doing it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1230 lines
75 KiB
Python
1230 lines
75 KiB
Python
#!/usr/bin/env python3
|
||
"""PROCITY Lane F — v2 flag enforcement harness (round-6 F1).
|
||
|
||
The machine version of what the R5 review did by hand. Three things, headless, <2 min:
|
||
1. FLAGS-OFF REGRESSION — the enforcement arm of the prime law. Boot with no flags → the plan
|
||
fingerprint must equal the golden 0x3fa36874, the settled street view must stay within the v1
|
||
budget (≤300 draws / ≤200k tris) AND within tolerance of the v1.1 snapshot, every v2 subsystem
|
||
must report inactive, and there must be ZERO console errors. A v2 flag leaking into the default
|
||
boot trips this.
|
||
2. PER-FLAG SMOKE — for each landed flag (winmap, dig, roster, +plansrc when A lands): boot flag-on,
|
||
cross ≥2 chunks, enter one interior (dig: also open+close a riffle), 0 console errors.
|
||
3. ALL-ON COMBO (flag-composition law, decision #4): every landed flag on at once, same smoke.
|
||
|
||
Run: cd web && python3 -m http.server 8130 (or let this script spawn its own)
|
||
tools/.venv/bin/python tools/flags_check.py
|
||
Wired into tools/qa.sh as a WARN-level gate this round (strict in round 7). Exit 0 = all pass;
|
||
non-zero = a check failed (qa.sh reports it as a warning this round).
|
||
"""
|
||
import sys, json, time, socket, subprocess, pathlib
|
||
try:
|
||
from playwright.sync_api import sync_playwright
|
||
except ImportError:
|
||
sys.exit("playwright not installed — python3 -m venv tools/.venv && "
|
||
"tools/.venv/bin/pip install playwright && tools/.venv/bin/python -m playwright install chromium")
|
||
|
||
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
||
PORT = 8130
|
||
HOST = f'http://127.0.0.1:{PORT}'
|
||
SEED = 20261990
|
||
GOLDEN_HASH = 0x3fa36874 # frozen v2 determinism anchor (?classic boot) — UNCHANGED by the flips
|
||
GIG_GOLDEN = 0xb1d48ea1 # [R16] the DEFAULT boot's gig-augmented plan fingerprint (A's R16 corner-poster re-pin)
|
||
# flags-off DBG.shot('street_noon') snapshot. RE-PINNED at the R7 roster flip (prime-law amendment):
|
||
# old (v1.1, roster-off): 163 draws, 19000 tris
|
||
# new (R7, roster default-on): 164 draws, ~66000 tris
|
||
# Draws barely move — E1's ped sub-mesh merge makes each near-rig ~1 draw, which is exactly what let
|
||
# the roster flip to full density. Tris jump is the streamed crowd (each near-rig ~2.4k tris); it is
|
||
# inherently variable frame-to-frame (how many rigs are near at sample time), so the drift WARN below
|
||
# is DRAWS-ONLY — tris is guarded strictly by the STREET_TRIS_MAX hard cap, not a soft snapshot.
|
||
BASE_DRAWS, BASE_TRIS = 164, 66000
|
||
STREET_DRAWS_MAX, STREET_TRIS_MAX = 300, 200_000
|
||
KNOWN_FLAGS = ['winmap=1', 'dig=1', 'roster=stream'] # plansrc=osm auto-appended if Lane A landed it
|
||
# [R12] gig-night interior budget (ROUND12 law: ≤350 draws incl. the crowd — a room with a band and an
|
||
# audience in it is allowed more than a shop, which is what the ≤300 street/room numbers were tuned for).
|
||
GIG_DRAWS_MAX = 350
|
||
# `cover` is seeded per gig, so which door-charge path the default seed exercises is luck. Pin one seed
|
||
# per half of John's "free AND paid" ruling: 20261990's tonight is $10 (Screaming Utes at The Thornbury
|
||
# Hotel), 1234's is free (Dazza & The Dropbears at The Imperial). Re-pin if Lane A re-seeds the gig stream.
|
||
FREE_GIG_SEED = 1234
|
||
|
||
fails, warns = [], []
|
||
def FAIL(m): fails.append(m); print(f" \033[31m✗ FAIL\033[0m {m}")
|
||
def WARN(m): warns.append(m); print(f" \033[33m! WARN\033[0m {m}")
|
||
def OK(m): print(f" \033[32m✓\033[0m {m}")
|
||
def head(m): print(f"\n\033[1m{m}\033[0m")
|
||
|
||
def stature_ok(f):
|
||
# [R16 ledger #3] seated figs (the drummer — fig.userData.procitySeated) pass the no-giants floor at a
|
||
# SEATED band [0.9,1.5] m; standing figs stay [1.4,2.0]. A seated human fails the standing floor by design.
|
||
lo, hi = (0.9, 1.5) if f.get('seated') else (1.4, 2.0)
|
||
return lo <= f['stature'] <= hi
|
||
|
||
def port_up(port):
|
||
with socket.socket() as s:
|
||
s.settimeout(0.4)
|
||
return s.connect_ex(('127.0.0.1', port)) == 0
|
||
|
||
def ensure_server():
|
||
if port_up(PORT):
|
||
return None # reuse a running dev server
|
||
proc = subprocess.Popen(['python3', '-m', 'http.server', str(PORT)],
|
||
cwd=str(ROOT / 'web'), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||
for _ in range(50):
|
||
if port_up(PORT): return proc
|
||
time.sleep(0.1)
|
||
proc.terminate(); sys.exit(f"could not start http.server on :{PORT}")
|
||
|
||
# ── page helpers ─────────────────────────────────────────────────────────────
|
||
def new_page(p):
|
||
b = p.chromium.launch()
|
||
pg = b.new_page(viewport={'width': 1280, 'height': 720})
|
||
errs = []
|
||
pg.on('console', lambda m: errs.append(m.text) if m.type == 'error' else None)
|
||
pg.on('pageerror', lambda e: errs.append(str(e)))
|
||
return b, pg, errs
|
||
|
||
def boot(pg, query):
|
||
pg.goto(f'{HOST}/index.html?seed={SEED}&dbg=1' + (('&' + query) if query else ''))
|
||
pg.wait_for_function("window.DBG && window.DBG.ready === true", timeout=20000)
|
||
pg.evaluate("() => { const o=document.getElementById('pc-start'); if(o) o.style.display='none'; }")
|
||
try:
|
||
pg.wait_for_function("() => window.PROCITY && (!window.PROCITY.fleet || window.PROCITY.fleet.ready)", timeout=12000)
|
||
except Exception:
|
||
pass
|
||
|
||
def gigs_landed(p):
|
||
"""Boot ?gigs=1; landed iff Lane A's augmentation produced a schedule (else the flag is inert)."""
|
||
b, pg, _ = new_page(p)
|
||
try:
|
||
boot(pg, '')
|
||
return bool(pg.evaluate("() => !!(window.PROCITY.plan.gigs && window.PROCITY.plan.gigs.length)"))
|
||
except Exception:
|
||
return False
|
||
finally:
|
||
b.close()
|
||
|
||
def plansrc_landed(p):
|
||
"""Boot ?plansrc=osm; landed iff the shell exposes an 'osm' plan source (else the flag is ignored)."""
|
||
b, pg, _ = new_page(p)
|
||
try:
|
||
boot(pg, 'plansrc=osm')
|
||
src = pg.evaluate("() => (window.PROCITY.plan && (window.PROCITY.plan.source||window.PROCITY.plan.planSource)) "
|
||
"|| window.PROCITY.planSource || null")
|
||
return src == 'osm'
|
||
except Exception:
|
||
return False
|
||
finally:
|
||
b.close()
|
||
|
||
# ── the three checks ─────────────────────────────────────────────────────────
|
||
def classic_regression(p):
|
||
# [R16 — THE FLIP] The prime-law covenant MOVED from "flags off" to "?classic=1": the classic boot must
|
||
# stay byte-identical to the frozen v2 baseline FOREVER — synthetic fingerprint 0x3fa36874, zero gig
|
||
# layer, no opt-in flags, the R7 streamed roster (which IS the v2.0 baseline). Same assertions as the old
|
||
# flags-off gate, now booted with ?classic=1 (the four flipped flags are off, everything else v2-default).
|
||
head('CLASSIC REGRESSION (?classic=1 → the frozen v2 covenant)')
|
||
b, pg, errs = new_page(p)
|
||
try:
|
||
boot(pg, 'classic=1')
|
||
state = pg.evaluate("""() => {
|
||
const P=window.PROCITY, D=window.DBG;
|
||
D.shot('street_noon'); // fixed pose → comparable draws/tris
|
||
const i = D.info();
|
||
const c = P.citizens || {};
|
||
return { draws:i.drawCalls, tris:i.tris, mode:i.mode,
|
||
winmap: !!(P.winmap && P.winmap.active),
|
||
digActive: !!(P.interiorMode && P.interiorMode.digActive),
|
||
streamMode: !!c.streamMode, // R7: real roster-mode flag (was reading a nonexistent prop)
|
||
// [R12] the gig layer must be wholly absent flags-off: no state machine, no schedule
|
||
// in the plan, no pub converted, and none of Lane B's venue geometry in the scene.
|
||
gigState: !!P.gigs, planGigs: (P.plan.gigs||[]).length, posters: (P.plan.posters||[]).length,
|
||
pub: !!(P.plan.shops||[]).find(s => s.venue || s.type === 'pub'),
|
||
venueGeo: !!P.scene.getObjectByName('venue-presentation') };
|
||
}""")
|
||
# determinism anchor — run Lane A's selfcheck in-process (golden fingerprint)
|
||
r = subprocess.run(['node', 'web/js/citygen/selfcheck.js'], cwd=str(ROOT),
|
||
capture_output=True, text=True)
|
||
hash_ok = (f'{GOLDEN_HASH:#010x}'.replace('0x', '0x') in r.stdout) or (hex(GOLDEN_HASH) in r.stdout) \
|
||
or ('3fa36874' in r.stdout)
|
||
# [R24] These are TWO assertions and they get two verdicts. This gate used to require
|
||
# `hash_ok and r.returncode == 0` and report BOTH failures with the fingerprint's message — so
|
||
# any unrelated red anywhere in Lane A's selfcheck (R24: 8 godverse-identity checks in a
|
||
# different town) screamed "determinism/prime-law broken", the loudest alarm we own, about a
|
||
# covenant that was provably intact (0x3fa36874, node-side, unmoved). A gate must name what it
|
||
# actually measured; F wrote this one, so F fixed it. The covenant is asserted on its own.
|
||
if hash_ok: OK(f'plan fingerprint == golden {hex(GOLDEN_HASH)} — the classic covenant holds')
|
||
else: FAIL(f'plan fingerprint != golden {hex(GOLDEN_HASH)} — determinism/prime-law broken')
|
||
if r.returncode == 0: OK('Lane A selfcheck green')
|
||
else:
|
||
tail = [l for l in r.stdout.splitlines() if l.strip().startswith('✗')][:3]
|
||
FAIL('Lane A selfcheck RED (separate from the covenant above): '
|
||
+ ('; '.join(t.strip() for t in tail) if tail else 'see selfcheck output'))
|
||
|
||
# winmap flips OFF under ?classic (it's default-on now); dig stays opt-in. The streamed roster IS the
|
||
# v2.0 baseline (R7 flip / prime-law amendment) — so it must be ON under ?classic.
|
||
if state['winmap'] or state['digActive']:
|
||
FAIL(f"a flipped/opt-in flag leaked into ?classic (winmap/dig): {state}")
|
||
else: OK('flipped + opt-in flags off under ?classic (winmap/dig)')
|
||
if state['streamMode']:
|
||
OK('R7 roster flip in effect — streamed roster ACTIVE under ?classic (the v2 baseline)')
|
||
else: FAIL('roster flip REGRESSED — ?classic is not streaming (expected streamMode:true)')
|
||
|
||
# [R12/R16] gig layer wholly absent under ?classic. Any of these present means the gig layer escaped
|
||
# the classic gate and the frozen v2 covenant is broken.
|
||
gig_leaks = [k for k in ('gigState', 'planGigs', 'posters', 'pub', 'venueGeo') if state[k]]
|
||
if not gig_leaks:
|
||
OK('v3 gig layer wholly absent under ?classic (no state machine, no plan.gigs/posters, no pub, no venue geometry)')
|
||
else:
|
||
FAIL(f"v3 gig layer LEAKED into ?classic: {[(k, state[k]) for k in gig_leaks]}")
|
||
|
||
if state['draws'] <= STREET_DRAWS_MAX and state['tris'] <= STREET_TRIS_MAX:
|
||
OK(f"?classic street view within budget (draws {state['draws']}≤{STREET_DRAWS_MAX}, tris {state['tris']}≤{STREET_TRIS_MAX})")
|
||
else: FAIL(f"?classic street view OVER budget: draws {state['draws']}, tris {state['tris']}")
|
||
|
||
# Drift WARN is DRAWS-ONLY — tris is crowd-variable post-flip (guarded by the hard cap above).
|
||
dd = abs(state['draws'] - BASE_DRAWS) / BASE_DRAWS
|
||
if dd <= 0.40:
|
||
OK(f"draws within 40% of re-pinned baseline ({state['draws']} vs {BASE_DRAWS}); tris {state['tris']} (crowd-variable, ≤cap)")
|
||
else:
|
||
WARN(f"draws drifted >40% from baseline ({state['draws']} vs {BASE_DRAWS}) — investigate")
|
||
|
||
if errs: FAIL(f"{len(errs)} console error(s) under ?classic; first: {errs[0][:140]}")
|
||
else: OK('0 console errors under ?classic')
|
||
|
||
# R7 escape hatch: ?roster=v1 must restore the fixed roster (streamMode:false), render, no errors.
|
||
b2, pg2, errs2 = new_page(p)
|
||
try:
|
||
boot(pg2, 'roster=v1')
|
||
esc = pg2.evaluate("() => ({ streamMode: !!(window.PROCITY.citizens||{}).streamMode, mode: window.DBG.info().mode })")
|
||
if not esc['streamMode'] and esc['mode'] == 'street':
|
||
OK('escape hatch ?roster=v1 → fixed roster (streamMode:false), renders')
|
||
else: FAIL(f"escape hatch ?roster=v1 broken: {esc}")
|
||
if errs2: FAIL(f"?roster=v1: {len(errs2)} console error(s); first: {errs2[0][:140]}")
|
||
finally:
|
||
b2.close()
|
||
finally:
|
||
b.close()
|
||
|
||
|
||
def default_boot_gate(p):
|
||
# [R16 — THE FLIP] The enforcement arm of the DEFAULT law: the no-flag boot is now the full experience —
|
||
# all four flags on, the gig layer present, deterministic to the gig golden. This is the number every
|
||
# budget/smoke now means (its inverse is classic_regression above).
|
||
head('DEFAULT-BOOT GATE (the flip → the town shows its best by default)')
|
||
b, pg, errs = new_page(p)
|
||
try:
|
||
boot(pg, '')
|
||
state = pg.evaluate("""() => {
|
||
const P = window.PROCITY, D = window.DBG;
|
||
D.setSegment(5); D.shot('venue_night'); // NIGHT at a venue → the busiest default view (gigs+pools+winmap+tram)
|
||
const i = D.info();
|
||
return { flags: P.flags,
|
||
gigState: !!P.gigs, planGigs: (P.plan.gigs||[]).length, posters: (P.plan.posters||[]).length,
|
||
pub: !!(P.plan.shops||[]).find(s => s.venue), venueGeo: !!P.scene.getObjectByName('venue-presentation'),
|
||
winmapActive: !!(P.winmap && P.winmap.active), tram: !!P.scene.getObjectByName('tram'),
|
||
draws: i.drawCalls, tris: i.tris };
|
||
}""")
|
||
f = state['flags'] or {}
|
||
if f.get('gigs') and f.get('weather') and f.get('winmap') and f.get('tram') and not f.get('classic'):
|
||
OK('default boot flips all four ON (gigs · weather · winmap · tram), classic off')
|
||
else:
|
||
FAIL(f"default boot flag intent wrong: {f}")
|
||
present = [k for k in ('gigState', 'planGigs', 'posters', 'pub', 'venueGeo') if state[k]]
|
||
if len(present) == 5 and state['tram']:
|
||
OK(f"the town is live: gig layer present ({state['planGigs']} gigs, {state['posters']} posters, venue geo) · tram up (winmap is a window shader — its own smoke)")
|
||
else:
|
||
FAIL(f"default boot not fully live: gig={present}, tram={state['tram']}")
|
||
# The default boot's plan IS the gig-augmented plan; selfcheck pins its fingerprint to the gig golden
|
||
# (0xb1d48ea1) INTERNALLY and is ALL GREEN only if it matches. (selfcheck prints the base fingerprint
|
||
# in its summary but asserts the gig golden silently — so we gate on the green run, not a substring.)
|
||
r = subprocess.run(['node', 'web/js/citygen/selfcheck.js'], cwd=str(ROOT), capture_output=True, text=True)
|
||
if r.returncode == 0 and 'ALL GREEN' in r.stdout:
|
||
OK(f"default plan pins to the gig golden {hex(GIG_GOLDEN)} (selfcheck ALL GREEN — the gig-layer golden asserts pass)")
|
||
else:
|
||
FAIL(f"selfcheck not green — the gig golden {hex(GIG_GOLDEN)} or determinism broke: {r.stdout[-160:]}")
|
||
# budget on the DEFAULT boot at the busiest venue block (the number the budget law now means)
|
||
if state['draws'] <= STREET_DRAWS_MAX and state['tris'] <= STREET_TRIS_MAX:
|
||
OK(f"default-boot street budget @ venue block: {state['draws']} draws ≤ {STREET_DRAWS_MAX}, {state['tris']:,} tris ≤ {STREET_TRIS_MAX:,}")
|
||
else:
|
||
FAIL(f"default-boot street OVER budget @ venue block: {state['draws']} draws / {state['tris']:,} tris (Fable scope call — not silent-trim)")
|
||
if errs: FAIL(f"{len(errs)} console error(s) on the default boot; first: {errs[0][:140]}")
|
||
else: OK('0 console errors on the default boot')
|
||
finally:
|
||
b.close()
|
||
|
||
|
||
def smoke(p, label, query, is_combo=False):
|
||
head(f'SMOKE: {label} (?{query})')
|
||
b, pg, errs = new_page(p)
|
||
try:
|
||
boot(pg, query)
|
||
# cross ≥2 chunks by teleporting between separated street nodes
|
||
chunks = pg.evaluate("""() => {
|
||
const P=window.PROCITY, D=window.DBG, seen=new Set();
|
||
const ns = P.plan.streets.nodes;
|
||
for (const idx of [0, (ns.length/3|0), (2*ns.length/3|0), ns.length-1]) {
|
||
const n = ns[idx]; D.teleport(n.x, n.z, 0);
|
||
for (let k=0;k<10;k++) (P.citizens&&P.citizens.update&&P.citizens.update(0.05));
|
||
const c = D.info().chunk; if (c!=null) seen.add(String(c));
|
||
}
|
||
return seen.size;
|
||
}""")
|
||
if chunks >= 2: OK(f'crossed {chunks} chunks streaming (flag active, no crash)')
|
||
else: WARN(f'only {chunks} distinct chunks seen (teleport/stream probe)')
|
||
|
||
# enter one interior (open shop), exit
|
||
entered = pg.evaluate("""() => {
|
||
const P=window.PROCITY, D=window.DBG;
|
||
D.setSegment(2);
|
||
const s=(P.plan.shops||[]).find(x=>x.type==='record' && P.isOpen(x)) || (P.plan.shops||[]).find(x=>P.isOpen(x));
|
||
if(!s) return {ok:false};
|
||
D.enterShop(s.id);
|
||
const inMode = D.info().mode==='interior';
|
||
return {ok:inMode, shop:s.name};
|
||
}""")
|
||
if entered.get('ok'): OK(f"entered interior '{entered.get('shop')}' (mode=interior)")
|
||
else: FAIL('could not enter an interior')
|
||
|
||
# dig-specific: open one riffle + close, leak-free
|
||
if 'dig=1' in query:
|
||
dug = pg.evaluate("""() => {
|
||
const P=window.PROCITY, THREE=P.THREE, room=P.interiorMode.current;
|
||
if(!room) return {ok:false, why:'no room'};
|
||
const bins=[]; room.group.traverse(o=>{ if(o.userData&&o.userData.kind==='bin') bins.push(o); });
|
||
if(!bins.length) return {ok:false, why:'no bins'};
|
||
const bp=new THREE.Vector3(); bins[0].getWorldPosition(bp);
|
||
P.camera.position.set(bp.x,1.6,bp.z+1.8); P.camera.lookAt(bp.x,bp.y+0.4,bp.z); P.camera.updateMatrixWorld();
|
||
window.dispatchEvent(new KeyboardEvent('keydown',{code:'KeyE'}));
|
||
const opened = P.interiorMode.digActive;
|
||
const btn=document.querySelector('.pcdg-out'); if(btn) btn.click();
|
||
return { ok: opened, closed: !P.interiorMode.digActive };
|
||
}""")
|
||
pg.wait_for_timeout(300)
|
||
if dug.get('ok') and dug.get('closed'): OK('dig: riffle opened (E on bin) + closed (WALK OUT)')
|
||
elif dug.get('ok'): WARN(f"dig: opened but close not confirmed ({dug})")
|
||
else: FAIL(f"dig: riffle did not open ({dug.get('why','?')})")
|
||
pg.evaluate("() => window.DBG.exitShop && window.DBG.exitShop()")
|
||
|
||
if errs: FAIL(f"{label}: {len(errs)} console error(s); first: {errs[0][:140]}")
|
||
else: OK(f'{label}: 0 console errors')
|
||
finally:
|
||
b.close()
|
||
|
||
def interior_budget(p):
|
||
"""Decision #2: interior draw budget is law — ≤350 draws worst room. Sweep every shop type
|
||
(dig on, so this also confirms batching kept ?dig=1 riffling). C enforces via its sweep; F pins it."""
|
||
head('INTERIOR DRAW BUDGET (≤350 draws/room law — decision #2)')
|
||
b, pg, errs = new_page(p)
|
||
try:
|
||
boot(pg, 'dig=1')
|
||
pg.evaluate("() => window.DBG.setSegment(2)") # midday → shops open
|
||
types = ['record', 'opshop', 'toy', 'book', 'video', 'pawn', 'milkbar', 'dept', 'stall']
|
||
rows, seen = [], set()
|
||
for t in types:
|
||
picked = pg.evaluate("""(t) => {
|
||
const P=window.PROCITY, D=window.DBG;
|
||
const s=(P.plan.shops||[]).find(x=>x.type===t && P.isOpen(x));
|
||
if(!s) return null; D.enterShop(s.id); return {id:s.id, name:s.name};
|
||
}""", t)
|
||
if not picked:
|
||
continue
|
||
pg.wait_for_timeout(500) # let async GLB fittings load → settled worst-case draws
|
||
info = pg.evaluate("() => { const i=window.DBG.info(); return {draws:i.drawCalls, tris:i.tris, mode:i.mode}; }")
|
||
rows.append({'type': t, 'shop': picked['name'], **info})
|
||
pg.evaluate("() => window.DBG.exitShop && window.DBG.exitShop()")
|
||
pg.wait_for_timeout(150)
|
||
over = [r for r in rows if r['draws'] > 350]
|
||
for r in over:
|
||
FAIL(f"interior {r['type']} '{r['shop']}': {r['draws']} draws > 350")
|
||
if rows and not over:
|
||
worst = max(rows, key=lambda r: r['draws'])
|
||
OK(f"all {len(rows)} room types ≤350 draws (worst: {worst['type']} {worst['draws']}, tris {worst['tris']})")
|
||
elif not rows:
|
||
WARN('no interiors sampled (no open shops of the swept types)')
|
||
if errs:
|
||
FAIL(f"interior sweep: {len(errs)} console error(s); first: {errs[0][:140]}")
|
||
finally:
|
||
b.close()
|
||
|
||
def _enter_type(pg, t, extra=''):
|
||
"""Enter an open shop of type `t`; returns the shop name or {err}."""
|
||
return pg.evaluate("""(t) => {
|
||
const P=window.PROCITY, D=window.DBG; D.setSegment(2);
|
||
const s=(P.plan.shops||[]).find(x=>x.type===t && P.isOpen(x));
|
||
if(!s) return {err:'no open '+t+' shop'};
|
||
D.enterShop(s.id); return { shop: s.name };
|
||
}""", t)
|
||
|
||
def _pack_ready(pg, t):
|
||
try:
|
||
pg.wait_for_function("async () => { const m = await import('./js/interiors/interiors.js');"
|
||
f" return !!(m.getStockPack && m.getStockPack('{t}')); }}", timeout=12000)
|
||
return True
|
||
except Exception:
|
||
return False
|
||
|
||
def smoke_stock(p):
|
||
"""GRADUATED STRICT (R8): ?stock=real → real GODVERSE sleeves (batched userData.isStock atlas meshes)."""
|
||
head('SMOKE: stock=real (?stock=real — real record sleeves; STRICT r8)')
|
||
b, pg, errs = new_page(p)
|
||
try:
|
||
boot(pg, 'stock=real&dig=1')
|
||
ready = _pack_ready(pg, 'record')
|
||
r = _enter_type(pg, 'record')
|
||
if r.get('err'): FAIL(f"stock=real: {r['err']}"); return
|
||
n = pg.evaluate("() => { let n=0; window.PROCITY.interiorMode.current.group.traverse(o=>{ if(o.userData&&o.userData.isStock&&o.material&&o.material.map) n++; }); return n; }")
|
||
if not ready: FAIL(f"stock=real: record pack didn't preload in 12s (E ships it) — got {n} isStock meshes")
|
||
elif n > 0: OK(f"stock=real: {n} real atlas sleeve mesh(es) in '{r['shop']}' — non-parody, batched")
|
||
else: FAIL(f"stock=real: pack loaded but 0 isStock atlas meshes in '{r['shop']}'")
|
||
if errs: FAIL(f"stock=real: {len(errs)} console error(s); first: {errs[0][:140]}")
|
||
finally:
|
||
b.close()
|
||
|
||
def smoke_weather(p):
|
||
"""GRADUATED STRICT (R8): ?weather=rain → rain THREE.Points layer + PROCITY.weather.state=='rain'."""
|
||
head('SMOKE: weather (?weather=rain — rain particles + wet ground; STRICT r8)')
|
||
b, pg, errs = new_page(p)
|
||
try:
|
||
boot(pg, 'weather=rain')
|
||
st = pg.evaluate("() => { const P=window.PROCITY; return { state: P.weather&&P.weather.state, "
|
||
"intensity: P.weather&&P.weather.intensity, rainPts: !!P.scene.getObjectByProperty('type','Points') }; }")
|
||
if st['state'] == 'rain' and st['rainPts']:
|
||
OK(f"weather=rain: rain Points present + PROCITY.weather.state=='rain' (intensity {st['intensity']})")
|
||
else: FAIL(f"weather=rain broken: {st}")
|
||
if errs: FAIL(f"weather=rain: {len(errs)} console error(s); first: {errs[0][:140]}")
|
||
finally:
|
||
b.close()
|
||
|
||
def smoke_buy(p):
|
||
"""NEW (warn): buy-v0 — open dig, pull + BUY a sleeve, assert wallet cash dropped + item banked."""
|
||
head('SMOKE: buy-v0 (dig BUY → session wallet; STRICT r9)')
|
||
b, pg, errs = new_page(p)
|
||
try:
|
||
boot(pg, 'dig=1')
|
||
setup = pg.evaluate("""() => {
|
||
const P=window.PROCITY, D=window.DBG; D.setSegment(2);
|
||
const rec=(P.plan.shops||[]).find(s=>s.type==='record' && P.isOpen(s));
|
||
if(!rec) return {err:'no record shop'};
|
||
D.enterShop(rec.id);
|
||
const bins=[]; P.interiorMode.current.group.traverse(o=>{if(o.userData&&o.userData.kind==='bin')bins.push(o)});
|
||
if(!bins.length) return {err:'no bins'};
|
||
const bp=new P.THREE.Vector3(); bins[0].getWorldPosition(bp);
|
||
P.camera.position.set(bp.x,1.6,bp.z+1.4); P.camera.lookAt(bp.x,bp.y+0.4,bp.z); P.camera.updateMatrixWorld();
|
||
return { cash0: P.wallet.cash(), count0: P.wallet.count() };
|
||
}""")
|
||
if setup.get('err'): FAIL(f"buy-v0: {setup['err']}"); return
|
||
pg.evaluate("() => window.dispatchEvent(new KeyboardEvent('keydown',{code:'KeyE'}))") # open dig
|
||
pg.wait_for_timeout(600)
|
||
# pull the front sleeve (canvas click, no drag → dig.pull) then click its BUY IT button
|
||
pg.evaluate("""() => { const c=window.PROCITY.renderer.domElement;
|
||
c.dispatchEvent(new PointerEvent('pointerdown',{clientX:640,clientY:360,bubbles:true}));
|
||
c.dispatchEvent(new PointerEvent('pointerup',{clientX:640,clientY:360,bubbles:true})); }""")
|
||
pg.wait_for_timeout(400)
|
||
res = pg.evaluate("""() => {
|
||
const w=window.PROCITY.wallet, cash0=w.cash(), count0=w.count();
|
||
const btn=document.querySelector('.pcdg-panel button');
|
||
if(!btn) return {err:'no BUY panel (pull failed)'};
|
||
if(btn.disabled) return {err:'BUY disabled (broke?)'};
|
||
btn.click();
|
||
return { cash0, count0, cash1:w.cash(), count1:w.count() };
|
||
}""")
|
||
if res.get('err'): FAIL(f"buy-v0: {res['err']}")
|
||
elif res['cash1'] < res['cash0'] and res['count1'] == res['count0'] + 1:
|
||
OK(f"buy-v0: bought 1 — cash {res['cash0']}→{res['cash1']}, bag {res['count0']}→{res['count1']}")
|
||
else: FAIL(f"buy-v0: unexpected wallet delta {res}")
|
||
if errs: FAIL(f"buy-v0: {len(errs)} console error(s); first: {errs[0][:140]}")
|
||
finally:
|
||
b.close()
|
||
|
||
def smoke_patronage(p):
|
||
"""NEW (warn): peds duck into open shops — assert ≥1 ped parks at a shop door over N seconds."""
|
||
head('SMOKE: patronage (peds visit open shops; STRICT r9)')
|
||
b, pg, errs = new_page(p)
|
||
try:
|
||
boot(pg, '') # default: streamed roster + patronage on
|
||
# advance the sim ~12s of ticks at a busy midday node; count peds flagged as inside/at-door
|
||
res = pg.evaluate("""() => {
|
||
const P=window.PROCITY, D=window.DBG; D.setSegment(2);
|
||
const plan=P.plan, deg={}; for(const e of plan.streets.edges){deg[e.a]=(deg[e.a]||0)+1;deg[e.b]=(deg[e.b]||0)+1;}
|
||
let best=null,bd=0; for(const n of plan.streets.nodes){const d=deg[n.id]||0;if(d>bd){bd=d;best=n;}}
|
||
D.teleport(best.x,best.z,0); P.citizens.setPopulation(200);
|
||
let everInside=0; const seen=new Set();
|
||
for(let i=0;i<360;i++){ P.citizens.update(0.05);
|
||
const list=P.citizens._activeList||[];
|
||
for(const c of list){ if(c && (c.patronInside||c.inside||c.patronTarget) && !seen.has(c.id)){ seen.add(c.id); everInside++; } }
|
||
}
|
||
return { active:(P.citizens._activeList||[]).length, everVisited: everInside, patronOn: P.citizens.patronageOn };
|
||
}""")
|
||
if not res['patronOn']: FAIL("patronage: off (patronageOn=false) — expected default-on")
|
||
elif res['everVisited'] > 0: OK(f"patronage: {res['everVisited']} ped(s) headed to/into a shop door over ~18s (active {res['active']})")
|
||
else: FAIL(f"patronage: 0 peds visited a shop over ~18s (active {res['active']}) — check door points / timing")
|
||
if errs: FAIL(f"patronage: {len(errs)} console error(s); first: {errs[0][:140]}")
|
||
finally:
|
||
b.close()
|
||
|
||
def smoke_booktoy(p):
|
||
"""NEW (warn): book/toy packs — ?stock=real static shelf stock (real spine/box atlas meshes)."""
|
||
head('SMOKE: book/toy stock (?stock=real — real spines/boxes; STRICT r9)')
|
||
b, pg, errs = new_page(p)
|
||
try:
|
||
boot(pg, 'stock=real')
|
||
for t in ('book', 'toy'):
|
||
_pack_ready(pg, t)
|
||
r = _enter_type(pg, t)
|
||
if r.get('err'): FAIL(f"book/toy: {r['err']}"); continue
|
||
# R9: real static stock is EITHER an isStock atlas mesh (records/toys) OR a buyMesh shelf
|
||
# (book spines / toy boxes, C2 buy-anywhere). Parody stock has neither.
|
||
n = pg.evaluate("""() => { let n=0; window.PROCITY.interiorMode.current.group.traverse(o=>{
|
||
const u=o.userData||{}; if((u.isStock && o.material && o.material.map) || (u.buyMesh && u.buyItems && u.buyItems.length)) n++; }); return n; }""")
|
||
if n > 0: OK(f"{t} stock: {n} real stock mesh(es) in '{r['shop']}'")
|
||
else: FAIL(f"{t} stock: 0 isStock atlas meshes in '{r['shop']}' (pack missing → parody fallback?)")
|
||
pg.evaluate("() => window.DBG.exitShop && window.DBG.exitShop()")
|
||
pg.wait_for_timeout(150)
|
||
if errs: FAIL(f"book/toy: {len(errs)} console error(s); first: {errs[0][:140]}")
|
||
finally:
|
||
b.close()
|
||
|
||
def smoke_presence(p):
|
||
"""NEW (warn): interior presence — enter an OCCUPIED shop, assert browser rigs == min(occupancy,3),
|
||
dispose clean on exit, 0 errors (the C→D→F seam)."""
|
||
head('SMOKE: interior presence (browsers in occupied shops; new → warn-level)')
|
||
b, pg, errs = new_page(p)
|
||
try:
|
||
boot(pg, 'stock=real')
|
||
res = pg.evaluate("""() => {
|
||
const P=window.PROCITY, D=window.DBG; D.setSegment(2);
|
||
const plan=P.plan, deg={}; for(const e of plan.streets.edges){deg[e.a]=(deg[e.a]||0)+1;deg[e.b]=(deg[e.b]||0)+1;}
|
||
let best=null,bd=0; for(const n of plan.streets.nodes){const d=deg[n.id]||0;if(d>bd){bd=d;best=n;}}
|
||
D.teleport(best.x,best.z,0); P.citizens.setPopulation(200);
|
||
for(let i=0;i<400;i++) P.citizens.update(0.05);
|
||
let shop=null,occ=null;
|
||
for(const s of plan.shops){ const o=P.citizens.occupancyOf(s.id); if(o&&o.count>0){shop=s;occ=o;break;} }
|
||
if(!shop) return {err:'no occupied shop after patronage'};
|
||
D.enterShop(shop.id);
|
||
const kl=P.interiorMode.keepers.keepers||[];
|
||
const total=kl.length, pts=(P.interiorMode.current.browsePoints||[]).length;
|
||
return { shop:shop.name, occ:occ.count, expect:Math.min(occ.count,3), totalKeepers:total, browsePoints:pts };
|
||
}""")
|
||
if res.get('err'): WARN(f"presence: {res['err']}"); return
|
||
# total keepers = 1 counter + browsers; browsers should == min(occ,3)
|
||
browsers = res['totalKeepers'] - 1
|
||
if browsers == res['expect'] and res['browsePoints'] >= 1:
|
||
OK(f"presence: {browsers} browser(s) in '{res['shop']}' == min(occupancy {res['occ']},3); {res['browsePoints']} browse points")
|
||
else:
|
||
WARN(f"presence: browsers {browsers} != expect {res['expect']} (occ {res['occ']}, pts {res['browsePoints']}, keepers {res['totalKeepers']})")
|
||
clean = pg.evaluate("() => { window.DBG.exitShop(); return (window.PROCITY.interiorMode.keepers.keepers||[]).length; }")
|
||
if clean == 0: OK("presence: browsers disposed clean on exit (0 keepers)")
|
||
else: WARN(f"presence: {clean} keeper(s) leaked after exit")
|
||
if errs: WARN(f"presence: {len(errs)} console error(s); first: {errs[0][:140]}")
|
||
finally:
|
||
b.close()
|
||
|
||
def smoke_shelfbuy(p):
|
||
"""NEW (warn): buy-anywhere — E aimed at a book spine / toy box buys over the wallet (Lane C C2)."""
|
||
head('SMOKE: shelf-buy (?stock=real book/toy pull-and-buy; new → warn-level)')
|
||
b, pg, errs = new_page(p)
|
||
try:
|
||
boot(pg, 'stock=real')
|
||
_pack_ready(pg, 'book')
|
||
res = pg.evaluate("""() => {
|
||
const P=window.PROCITY, D=window.DBG; D.setSegment(2);
|
||
const bk=(P.plan.shops||[]).find(s=>s.type==='book' && P.isOpen(s));
|
||
if(!bk) return {err:'no book shop'};
|
||
D.enterShop(bk.id);
|
||
const meshes=[]; P.interiorMode.current.group.traverse(o=>{ if(o.userData&&o.userData.buyMesh&&o.userData.buyItems&&o.userData.buyItems.length) meshes.push(o); });
|
||
if(!meshes.length) return {err:'no buy meshes (pack not loaded?)'};
|
||
const m=meshes[0], it=m.userData.buyItems[0], wp=new P.THREE.Vector3();
|
||
m.localToWorld(wp.copy(it.center));
|
||
P.camera.position.set(wp.x,wp.y,wp.z+1.0); P.camera.lookAt(wp.x,wp.y,wp.z); P.camera.updateMatrixWorld();
|
||
const cash0=P.wallet.cash(), count0=P.wallet.count();
|
||
window.dispatchEvent(new KeyboardEvent('keydown',{code:'KeyE'}));
|
||
return { shop:bk.name, buyMeshes:meshes.length, cash0, count0, cash1:P.wallet.cash(), count1:P.wallet.count() };
|
||
}""")
|
||
if res.get('err'): WARN(f"shelf-buy: {res['err']}")
|
||
elif res['cash1'] < res['cash0'] and res['count1'] == res['count0'] + 1:
|
||
OK(f"shelf-buy: bought a book spine in '{res['shop']}' — cash {res['cash0']}→{res['cash1']}, bag {res['count0']}→{res['count1']} ({res['buyMeshes']} buy meshes)")
|
||
else: WARN(f"shelf-buy: no purchase — {res}")
|
||
if errs: WARN(f"shelf-buy: {len(errs)} console error(s); first: {errs[0][:140]}")
|
||
finally:
|
||
b.close()
|
||
|
||
def smoke_tram(p):
|
||
"""NEW (warn): ?tram=1 — Lane B tram loop. Auto-skips if not landed."""
|
||
b, pg, errs = new_page(p)
|
||
try:
|
||
boot(pg, 'tram=1')
|
||
present = pg.evaluate("() => !!window.PROCITY.scene.getObjectByName('tram')")
|
||
if not present:
|
||
print("· tram=1 not landed (no 'tram' in scene) — skipping (Lane B)")
|
||
return
|
||
head('SMOKE: tram (?tram=1; new → warn-level)')
|
||
OK("tram=1: 'tram' present in scene")
|
||
if errs: WARN(f"tram: {len(errs)} console error(s); first: {errs[0][:140]}")
|
||
finally:
|
||
b.close()
|
||
|
||
def smoke_audio(p):
|
||
"""R11 audio house-law (Lane F): silent-and-happy, nothing plays pre-gesture, ?mute=1 silences,
|
||
?noassets=1 fetches zero audio, and interior beds play + release AudioNodes across enter/exit.
|
||
Each arm uses a fresh Chromium context (standing law — a reused tab's module cache burned us in R10)."""
|
||
head('SMOKE: audio (R11 — silent-happy · pre-gesture silence · mute · noassets · leak)')
|
||
|
||
# 1) boot-with-audio: 0 console errors, and nothing plays before a gesture (ctx suspended → state.ready false)
|
||
b, pg, errs = new_page(p)
|
||
try:
|
||
boot(pg, '')
|
||
pg.wait_for_function("() => window.PROCITY && window.PROCITY.audio && window.PROCITY.audio.state", timeout=8000)
|
||
st = pg.evaluate("() => { const s=window.PROCITY.audio.state; return {ready:!!s.ready, muted:!!s.muted, mode:s.mode}; }")
|
||
if not st['ready']:
|
||
OK(f"pre-gesture: audio NOT unlocked (state.ready=false, mode={st['mode']}) — silent until first gesture")
|
||
else:
|
||
FAIL(f"audio unlocked before any user gesture (autoplay-policy violation): {st}")
|
||
if errs: FAIL(f"audio boot: {len(errs)} console error(s); first: {errs[0][:140]}")
|
||
else: OK("audio boot: 0 console errors (silent-and-happy)")
|
||
finally:
|
||
b.close()
|
||
|
||
# 2) ?mute=1 → engine reports muted
|
||
b, pg, errs = new_page(p)
|
||
try:
|
||
boot(pg, 'mute=1')
|
||
pg.wait_for_function("() => window.PROCITY && window.PROCITY.audio", timeout=8000)
|
||
st = pg.evaluate("() => ({muted:!!window.PROCITY.audio.state.muted, mode:window.PROCITY.audio.state.mode})")
|
||
if st['muted']: OK(f"?mute=1: engine muted (mode={st['mode']})")
|
||
else: FAIL(f"?mute=1 not honored: {st}")
|
||
if errs: FAIL(f"?mute=1: {len(errs)} console error(s)")
|
||
finally:
|
||
b.close()
|
||
|
||
# 3) ?noassets=1 → zero audio network fetches (house law: the noassets gate implies no audio)
|
||
b, pg, errs = new_page(p)
|
||
audio_reqs = []
|
||
pg.on('request', lambda r: audio_reqs.append(r.url)
|
||
if ('/assets/audio/' in r.url or r.url.endswith('.ogg') or r.url.endswith('.m4a')) else None)
|
||
try:
|
||
boot(pg, 'noassets=1')
|
||
pg.keyboard.press('Space') # a keyboard gesture unlocks the AudioContext WITHOUT a pointer-lock attempt (headless-safe) # a gesture, so the audio path is genuinely exercised
|
||
pg.evaluate("""() => { const P=window.PROCITY, D=window.DBG; D.setSegment(2);
|
||
const s=(P.plan.shops||[]).find(x=>P.isOpen(x)); if(s) D.enterShop(s.id); }""")
|
||
pg.wait_for_timeout(800)
|
||
man = pg.evaluate("() => window.PROCITY.audio.state.manifest")
|
||
if len(audio_reqs) == 0: OK(f"?noassets=1: 0 audio fetches (manifest={man})")
|
||
else: FAIL(f"?noassets=1: {len(audio_reqs)} audio fetch(es) leaked; first {audio_reqs[0]}")
|
||
if errs: FAIL(f"?noassets=1 audio: {len(errs)} console error(s)")
|
||
finally:
|
||
b.close()
|
||
|
||
# 4) interior beds play + enter/exit is AudioNode-leak-free (live BufferSources stay bounded, not O(N))
|
||
b = p.chromium.launch()
|
||
pg = b.new_page(viewport={'width': 1280, 'height': 720})
|
||
errs = []
|
||
pg.on('console', lambda m: errs.append(m.text) if m.type == 'error' else None)
|
||
pg.on('pageerror', lambda e: errs.append(str(e)))
|
||
pg.add_init_script("""
|
||
window.__live = new Set(); window.__made = 0;
|
||
const AC = window.AudioContext || window.webkitAudioContext;
|
||
if (AC) { const orig = AC.prototype.createBufferSource;
|
||
AC.prototype.createBufferSource = function () { const s = orig.call(this);
|
||
window.__made++; window.__live.add(s);
|
||
const ostop = s.stop.bind(s); s.stop = function (w) { window.__live.delete(s); return ostop(w); };
|
||
s.addEventListener('ended', () => window.__live.delete(s)); return s; }; }
|
||
""")
|
||
try:
|
||
boot(pg, '')
|
||
pg.keyboard.press('Space') # a keyboard gesture unlocks the AudioContext WITHOUT a pointer-lock attempt (headless-safe) # user gesture → unlock the AudioContext
|
||
pg.wait_for_timeout(250)
|
||
played = pg.evaluate("""async () => {
|
||
const P=window.PROCITY, D=window.DBG; D.setSegment(2);
|
||
const types=['record','milkbar','video','opshop','book','toy'];
|
||
let entered=0, withMusic=0;
|
||
for (let rep=0; rep<2; rep++) for (const t of types) {
|
||
const s=(P.plan.shops||[]).find(x=>x.type===t && P.isOpen(x)); if(!s) continue;
|
||
D.enterShop(s.id); entered++;
|
||
const a = P.interiorMode.current && P.interiorMode.current.audio;
|
||
if (a && a.musicKey) withMusic++;
|
||
await new Promise(r=>setTimeout(r,120));
|
||
D.exitShop && D.exitShop(); await new Promise(r=>setTimeout(r,120));
|
||
}
|
||
return { entered, withMusic, ready:!!P.audio.state.ready };
|
||
}""")
|
||
pg.wait_for_timeout(600)
|
||
live = pg.evaluate("() => window.__live.size")
|
||
made = pg.evaluate("() => window.__made")
|
||
if not played['ready']:
|
||
WARN(f"audio interior: ctx never unlocked in headless — leak check inconclusive ({played}, live {live}/made {made})")
|
||
elif played['entered'] >= 4 and live <= 12:
|
||
OK(f"audio interior: {played['entered']} enter/exit, {played['withMusic']} with a music bed; live AudioNodes {live} bounded (made {made}) — leak-free")
|
||
else:
|
||
FAIL(f"audio interior: {live} live AudioNodes after {played['entered']} enter/exit (made {made}) — possible leak")
|
||
if errs: FAIL(f"audio interior: {len(errs)} console error(s); first: {errs[0][:140]}")
|
||
else: OK("audio interior: 0 console errors across enter/exit cycles")
|
||
finally:
|
||
b.close()
|
||
|
||
def smoke_gigs(p):
|
||
"""R13 v3.0-beta THE DISTRICT gig layer (Lane F). The ROUND13 §Lane F.6 gates — district determinism ·
|
||
a 4-piece band · per-venue crowd ≤ watchPoints · EVERY venue's gigKey resolves to a real manifest bed
|
||
(debt #1, now across 3 genres) · gig-night interior draw budget · street budget at the busiest venue
|
||
block · no-giants (STATURE) on the stage/crowd rigs · queue ≤ cap and drains by 'done' (pending Lane D)
|
||
· ?noassets=1 still silent-and-fine with ?gigs=1 — plus John's cover ruling, now PER VENUE (paid · free
|
||
· broke-knockback · no re-charge on re-entry · a night at two venues is two covers).
|
||
|
||
Seeds are pinned because `cover` is seeded: 20261990's tonight is a $10 night (Screaming Utes at The
|
||
Thornbury Hotel) and 1234's is free (Dazza & The Dropbears at The Imperial), so BOTH halves of "free
|
||
AND paid" are covered deterministically rather than whichever the default seed happened to roll."""
|
||
head('SMOKE: gigs (R13 v3.0-beta district — determinism · 4-piece · per-venue crowd/cover · gigKeys · budgets · queue · noassets)')
|
||
if not gigs_landed(p):
|
||
print('· gigs=1 not landed (no plan.gigs) — skipping (Lane A)')
|
||
return
|
||
|
||
# 1) schedule determinism — two independent boots (fresh contexts) → byte-equal gigs + posters
|
||
sigs = []
|
||
for _ in range(2):
|
||
b, pg, errs = new_page(p)
|
||
try:
|
||
boot(pg, '')
|
||
sigs.append(pg.evaluate("() => JSON.stringify({g: window.PROCITY.plan.gigs, p: window.PROCITY.plan.posters})"))
|
||
if errs: FAIL(f"gigs boot: {len(errs)} console error(s); first: {errs[0][:140]}")
|
||
finally:
|
||
b.close()
|
||
if sigs[0] == sigs[1]:
|
||
n = len(json.loads(sigs[0])['g'])
|
||
OK(f"gig schedule deterministic: two runs byte-equal ({n} nights + posters)")
|
||
else:
|
||
FAIL('gig schedule NOT deterministic across two runs (byte-diff in plan.gigs/plan.posters)')
|
||
|
||
# 2) the gig night itself: band · crowd · dance mix · budget · the live bed · no giants
|
||
b, pg, errs = new_page(p)
|
||
try:
|
||
boot(pg, '')
|
||
pg.keyboard.press('Space') # gesture → AudioContext unlock (headless-safe, no pointer-lock)
|
||
night = pg.evaluate("""async () => {
|
||
const P = window.PROCITY, D = window.DBG, THREE = P.THREE, v = new THREE.Vector3();
|
||
const id = P.gigs.venueShopIds[0]; // R15: no scalar alias — the primary venue by id
|
||
D.setSegment(5); // NIGHT — the gig is on
|
||
const state = P.gigs.stateOf(id);
|
||
D.enterShop(id);
|
||
await new Promise(r => setTimeout(r, 500));
|
||
const room = P.interiorMode.current;
|
||
if (!room) return { entered: false, state };
|
||
const crew = P.interiorMode.crew, info = P.interiorMode.crewInfo;
|
||
const mem = crew ? crew.members : [];
|
||
// STATURE, not world crown: the band stands on Lane C's stage deck (deckY ~0.32 m), so a
|
||
// perfectly human 1.73 m guitarist has a world crown of 2.05 m and a naive [1.4,2.0] world-Y
|
||
// check fails a correct band (it did, on the first R12 verify run). Measure crown MINUS the
|
||
// figure's own base y — that is what "human-sized" actually means — and keep the separate
|
||
// "never clips the ceiling" assert on the world crown, which is the real R10 failure mode.
|
||
const figs = mem.map(m => {
|
||
let crown = 0;
|
||
m.actor.fig.updateWorldMatrix(true, true);
|
||
(m.actor.inner || m.actor.fig).traverse(o => { if (o.isBone && /head/i.test(o.name)) {
|
||
o.getWorldPosition(v); crown = Math.max(crown, v.y); } });
|
||
return { part: m.part, kind: m.kind, dance: !!m.dance,
|
||
crown: +crown.toFixed(3), stature: +(crown - (m.base.y || 0)).toFixed(3),
|
||
seated: !!(m.actor.fig.userData && m.actor.fig.userData.procitySeated) }; // [R16 #3]
|
||
});
|
||
// Force one clean interior frame rather than trusting whatever the last rAF left in
|
||
// renderer.info — a throttled/backgrounded tab otherwise reports the last STREET frame
|
||
// (composer + bloom), which reads as a wildly over-budget room. Bit us on the first verify.
|
||
P.renderer.info.reset();
|
||
P.renderer.render(P.interiorMode.scene, P.camera);
|
||
return {
|
||
entered: true, state, H: +room.dims.H.toFixed(2),
|
||
watchPoints: room.watchPoints.length, stage: !!room.stage,
|
||
band: info ? info.band : 0, crowd: info ? info.crowd : 0,
|
||
dancers: mem.filter(m => m.part === 'crowd' && m.dance).length,
|
||
gigKey: room.audio.gigKey || null, musicKey: room.audio.musicKey || null,
|
||
draws: P.renderer.info.render.calls, tris: P.renderer.info.render.triangles, figs,
|
||
};
|
||
}""")
|
||
if not night.get('entered'):
|
||
FAIL(f"gig night: could not enter the venue at NIGHT ({night})")
|
||
else:
|
||
if night['state'] == 'on': OK(f"gig state machine: NIGHT → 'on' (venue {night['H']}m room)")
|
||
else: FAIL(f"gig state machine: NIGHT should be 'on', got '{night['state']}'")
|
||
# band is a 4-piece (R13 debt #3: front-line trio + a drummer on C's riser)
|
||
if night['band'] == 4: OK('band: 4-piece on the stage (front-line trio + drummer)')
|
||
else: FAIL(f"band: expected a 4-piece (trio + drummer), got {night['band']}")
|
||
# crowd cap ≤ watchPoints
|
||
if night['crowd'] <= night['watchPoints'] and night['crowd'] > 0:
|
||
OK(f"crowd: {night['crowd']} at {night['watchPoints']} watch points (≤ cap)")
|
||
else:
|
||
FAIL(f"crowd {night['crowd']} vs {night['watchPoints']} watch points (must be >0 and ≤ cap)")
|
||
# John's "a bit of both": the crowd must not be all-standing or all-dancing
|
||
d, c = night['dancers'], night['crowd']
|
||
if c and 0 < d < c: OK(f"crowd mix: {d} dancing / {c - d} standing — 'a bit of both'")
|
||
else: FAIL(f"crowd is not mixed: {d} dancing of {c} (John's ruling: some of both)")
|
||
# the live bed actually resolves (the C↔E gigKey seam F bridges — see LANE_F_NOTES §12)
|
||
if night['gigKey']: OK(f"live bed: room.audio.gigKey = {night['gigKey']} (F resolves → manifest key)")
|
||
else: FAIL('live bed: no room.audio.gigKey on a gig-on venue (Lane C opts.gig seam)')
|
||
# gig-night interior draw budget (ROUND12: ≤350 incl. the crowd)
|
||
if night['draws'] <= GIG_DRAWS_MAX:
|
||
OK(f"gig-night interior budget: {night['draws']} draws ≤ {GIG_DRAWS_MAX} (tris {night['tris']:,})")
|
||
else:
|
||
FAIL(f"gig-night interior budget: {night['draws']} draws > {GIG_DRAWS_MAX} (tris {night['tris']:,})")
|
||
# no giants on the stage/crowd rigs (R10 gate, inherited by the gig path)
|
||
rigs = [f for f in night['figs'] if f['kind'] == 'rig']
|
||
bad = [f for f in rigs if not stature_ok(f) or f['crown'] >= night['H']] # [R16 #3] seated exemption
|
||
if not rigs:
|
||
WARN(f"no-giants: crew spawned as placeholders (no fleet) — rig check n/a ({len(night['figs'])} figs)")
|
||
elif bad:
|
||
FAIL(f"no-giants: {len(bad)} gig rig(s) out of [1.4,2.0]m stature or through the {night['H']}m ceiling: {bad[:3]}")
|
||
else:
|
||
lo = min(f['stature'] for f in rigs); hi = max(f['stature'] for f in rigs)
|
||
hc = max(f['crown'] for f in rigs)
|
||
OK(f"no-giants: {len(rigs)} band+crowd rigs human-sized ({lo:.2f}–{hi:.2f} m stature; "
|
||
f"tallest crown {hc:.2f} m under the {night['H']} m ceiling)")
|
||
if errs: FAIL(f"gig night: {len(errs)} console error(s); first: {errs[0][:140]}")
|
||
else: OK('gig night: 0 console errors')
|
||
finally:
|
||
b.close()
|
||
|
||
# 2b) THE DISTRICT (R13): every playing venue fires — its live bed resolves to a REAL manifest bar for
|
||
# its genre (debt #1, now across pub/band_room/rsl), its crowd is capped at its OWN watch points, and it
|
||
# is a 4-piece. Plus the queue ≤ cap / drains-by-done probe (WARN-skips until Lane D wires the seam).
|
||
music_keys = set()
|
||
try:
|
||
mani = json.loads((ROOT / 'web' / 'assets' / 'manifest.json').read_text())
|
||
music_keys = set((mani.get('audio', {}).get('music', {}) or {}).keys())
|
||
except Exception as e:
|
||
WARN(f"district: could not read manifest music keys ({e}) — gigKey-resolves check is JS-only")
|
||
b, pg, errs = new_page(p)
|
||
try:
|
||
boot(pg, '')
|
||
pg.keyboard.press('Space')
|
||
district = pg.evaluate("""async () => {
|
||
const P = window.PROCITY, D = window.DBG, THREE = P.THREE, v = new THREE.Vector3();
|
||
D.setSegment(5); // NIGHT — every playing venue is 'on'
|
||
const ids = P.gigs.venueShopIds || [];
|
||
const out = [];
|
||
for (const id of ids) {
|
||
const shop = (P.plan.shops || []).find(s => s.id === id);
|
||
if (!P.gigs.onOf(id)) { out.push({ id, kind: shop && shop.venueKind, dark: true }); continue; }
|
||
D.enterShop(id);
|
||
await new Promise(r => setTimeout(r, 450));
|
||
const room = P.interiorMode.current, info = P.interiorMode.crewInfo;
|
||
const mem = P.interiorMode.crew ? P.interiorMode.crew.members : [];
|
||
const figs = mem.map(m => {
|
||
let crown = 0; m.actor.fig.updateWorldMatrix(true, true);
|
||
(m.actor.inner || m.actor.fig).traverse(o => { if (o.isBone && /head/i.test(o.name)) {
|
||
o.getWorldPosition(v); crown = Math.max(crown, v.y); } });
|
||
return { part: m.part, kind: m.kind, stature: +(crown - (m.base.y || 0)).toFixed(3),
|
||
seated: !!(m.actor.fig.userData && m.actor.fig.userData.procitySeated) }; // [R16 #3]
|
||
});
|
||
P.renderer.info.reset(); P.renderer.render(P.interiorMode.scene, P.camera);
|
||
out.push({ id, kind: shop && shop.venueKind, genreKey: shop && shop.genreKey,
|
||
gigKey: (room && room.audio && room.audio.gigKey) || null,
|
||
band: info ? info.band : 0, crowd: info ? info.crowd : 0,
|
||
watchPoints: room ? room.watchPoints.length : 0, figs });
|
||
D.exitShop(); await new Promise(r => setTimeout(r, 200));
|
||
}
|
||
return { ids, out };
|
||
}""")
|
||
playing = [d for d in district['out'] if not d.get('dark')]
|
||
OK(f"district: {len(district['ids'])} venue(s) — {len(playing)} playing tonight, "
|
||
f"{len(district['out']) - len(playing)} dark (reads true to the seeded week)")
|
||
genres_seen = set()
|
||
for d in playing:
|
||
tag = f"{d['kind']} #{d['id']}"
|
||
want = f"gig-{d['genreKey']}"
|
||
if d['gigKey'] == want and (not music_keys or want in music_keys):
|
||
OK(f" {tag}: live bed '{d['gigKey']}' resolves to a real manifest bar (genre {d['genreKey']})")
|
||
genres_seen.add(d['genreKey'])
|
||
else:
|
||
FAIL(f" {tag}: gigKey '{d['gigKey']}' != '{want}' or not in manifest.music {sorted(music_keys)}")
|
||
if 0 < d['crowd'] <= d['watchPoints']:
|
||
OK(f" {tag}: crowd {d['crowd']} ≤ {d['watchPoints']} watch points")
|
||
else:
|
||
FAIL(f" {tag}: crowd {d['crowd']} vs {d['watchPoints']} watch points (must be >0 and ≤ cap)")
|
||
if d['band'] == 4: OK(f" {tag}: 4-piece band")
|
||
else: FAIL(f" {tag}: expected a 4-piece, got {d['band']}")
|
||
rigs = [f for f in d['figs'] if f['kind'] == 'rig']
|
||
bad = [f for f in rigs if not stature_ok(f)] # [R16 #3] seated exemption
|
||
if rigs and bad: FAIL(f" {tag}: {len(bad)} giant rig(s) by stature: {bad[:2]}")
|
||
if len(genres_seen) >= 2:
|
||
OK(f"district: {len(genres_seen)} distinct genres verified live ({', '.join(sorted(genres_seen))})")
|
||
|
||
# queue ≤ cap and drains by 'done' — Lane D's outdoor line (charter item). D shipped the VenueQueue
|
||
# class and left the street-side lifecycle to F, which wires it in the shell (spawn at doors, drive
|
||
# each frame, disposeAll at done); read via P.queueCountOf(id). The line is a seeded short 2–6.
|
||
q = pg.evaluate("""async () => {
|
||
const P = window.PROCITY, D = window.DBG;
|
||
if (typeof P.queueCountOf !== 'function') return { wired: false };
|
||
const id = P.gigs.venueShopIds[0];
|
||
D.setSegment(4); await new Promise(r => setTimeout(r, 700)); // DUSK — doors open, the line forms
|
||
const atDoors = P.queueCountOf(id) | 0;
|
||
D.setSegment(0); await new Promise(r => setTimeout(r, 700)); // DAWN — closing time ('done'), line gone
|
||
const atDone = P.queueCountOf(id) | 0;
|
||
return { wired: true, atDoors, atDone };
|
||
}""")
|
||
QUEUE_CAP = 6
|
||
if not q.get('wired'):
|
||
FAIL("queue: P.queueCountOf(id) not exposed — F's shell wiring missing")
|
||
elif 0 < q['atDoors'] <= QUEUE_CAP and q['atDone'] == 0:
|
||
OK(f"queue: {q['atDoors']} punters in line at doors (≤ cap {QUEUE_CAP}), drained to 0 by 'done'")
|
||
else:
|
||
FAIL(f"queue: at doors {q['atDoors']} (want 1..{QUEUE_CAP}), at done {q['atDone']} (want 0)")
|
||
|
||
if errs: FAIL(f"district: {len(errs)} console error(s); first: {errs[0][:140]}")
|
||
else: OK('district: 0 console errors across all venues')
|
||
finally:
|
||
b.close()
|
||
|
||
# 2c) STREET BUDGET at the busiest venue block (R13 law: ≤300 draws / ≤200k tris with the lit frontage,
|
||
# town-wide posters and the queue zone all in). Park on the footpath in front of the primary venue at
|
||
# NIGHT with the gig on, let the block stream in, and sample the settled composer frame.
|
||
b, pg, errs = new_page(p)
|
||
try:
|
||
boot(pg, '')
|
||
st = pg.evaluate("""async () => {
|
||
const P = window.PROCITY, D = window.DBG;
|
||
D.setSegment(5); // NIGHT — frontage lit, posters up
|
||
const id = P.gigs.venueShopIds[0], shop = (P.plan.shops || []).find(s => s.id === id);
|
||
const lot = shop && (P.plan.lots || []).find(l => l.id === shop.lot);
|
||
if (lot) {
|
||
const fx = Math.sin(lot.ry || 0), fz = Math.cos(lot.ry || 0);
|
||
D.teleport(lot.x + fx * 10, lot.z + fz * 10, Math.atan2(-fx, -fz)); // 10 m out, facing the venue
|
||
}
|
||
await new Promise(r => setTimeout(r, 700)); // chunks/frontage/posters/pools settle
|
||
let draws = Infinity, tris = 0; // take the settled floor (ignore stream spikes)
|
||
for (let i = 0; i < 12; i++) {
|
||
await new Promise(r => requestAnimationFrame(r));
|
||
if (P.renderer.info.render.calls < draws) { draws = P.renderer.info.render.calls; tris = P.renderer.info.render.triangles; }
|
||
}
|
||
return { draws, tris, venue: shop && shop.name };
|
||
}""")
|
||
if st['draws'] <= STREET_DRAWS_MAX and st['tris'] <= STREET_TRIS_MAX:
|
||
OK(f"street budget @ venue block ({st['venue']}): {st['draws']} draws ≤ {STREET_DRAWS_MAX}, {st['tris']:,} tris ≤ {STREET_TRIS_MAX:,}")
|
||
else:
|
||
FAIL(f"street budget @ venue block ({st['venue']}): {st['draws']} draws / {st['tris']:,} tris over ({STREET_DRAWS_MAX} / {STREET_TRIS_MAX:,})")
|
||
if errs: FAIL(f"street budget: {len(errs)} console error(s); first: {errs[0][:140]}")
|
||
finally:
|
||
b.close()
|
||
|
||
# 3) cover ruling, PAID night (seed 20261990 → $10): debit once · no re-charge on re-entry · broke = knockback
|
||
b, pg, errs = new_page(p)
|
||
try:
|
||
boot(pg, '')
|
||
paid = pg.evaluate("""async () => {
|
||
const P = window.PROCITY, D = window.DBG;
|
||
const id = P.gigs.venueShopIds[0]; // R15: no scalar alias — the primary venue by id
|
||
D.setSegment(5);
|
||
const cover = P.gigs.coverOf(id);
|
||
if (!cover) return { skip: true, cover };
|
||
const before = P.wallet.cash();
|
||
D.enterShop(id); await new Promise(r => setTimeout(r, 350));
|
||
const inRoom1 = !!P.interiorMode.current, afterPay = P.wallet.cash();
|
||
D.exitShop(); await new Promise(r => setTimeout(r, 250));
|
||
const mid = P.wallet.cash();
|
||
D.enterShop(id); await new Promise(r => setTimeout(r, 350));
|
||
const inRoom2 = !!P.interiorMode.current, afterReentry = P.wallet.cash();
|
||
D.exitShop(); await new Promise(r => setTimeout(r, 250));
|
||
// now go broke and try again on the SAME night (stamp already paid → still free), then roll the
|
||
// night over (MORNING clears the stamp) and try broke → must be knocked back at the door.
|
||
P.wallet.buy({ title: 'qa: drain', price: P.wallet.cash() });
|
||
D.setSegment(1); D.setSegment(5); // NIGHT → MORNING → NIGHT = a new night, cover due
|
||
const brokeCash = P.wallet.cash();
|
||
D.enterShop(id); await new Promise(r => setTimeout(r, 350));
|
||
const inRoomBroke = !!P.interiorMode.current;
|
||
return { cover, before, afterPay, inRoom1, mid, afterReentry, inRoom2, brokeCash, inRoomBroke,
|
||
inv: P.wallet.inventory().map(i => i.title) };
|
||
}""")
|
||
if paid.get('skip'):
|
||
WARN(f"cover: seed {SEED} tonight is free (cover 0) — paid path not exercised here")
|
||
else:
|
||
if paid['inRoom1'] and paid['afterPay'] == paid['before'] - paid['cover']:
|
||
OK(f"cover: ${paid['cover']} debited once at the door (${paid['before']} → ${paid['afterPay']}), entry granted")
|
||
else:
|
||
FAIL(f"cover: wrong debit/entry on a paid night: {paid}")
|
||
if paid['inRoom2'] and paid['afterReentry'] == paid['mid']:
|
||
OK(f"cover: re-entry the same night is FREE (no re-charge, still ${paid['afterReentry']})")
|
||
else:
|
||
FAIL(f"cover: re-entry re-charged or was refused: {paid}")
|
||
if not paid['inRoomBroke'] and paid['brokeCash'] < paid['cover']:
|
||
OK(f"cover: broke (${paid['brokeCash']}) on a NEW night → polite knockback, no entry")
|
||
else:
|
||
FAIL(f"cover: broke player got in (or the night never rolled): {paid}")
|
||
if errs: FAIL(f"cover: {len(errs)} console error(s); first: {errs[0][:140]}")
|
||
finally:
|
||
b.close()
|
||
|
||
# 4) cover ruling, FREE night (seed 1234 → cover 0): walk straight in, wallet untouched
|
||
b, pg, errs = new_page(p)
|
||
try:
|
||
pg.goto(f'{HOST}/index.html?seed={FREE_GIG_SEED}&dbg=1')
|
||
pg.wait_for_function("window.DBG && window.DBG.ready === true", timeout=20000)
|
||
pg.evaluate("() => { const o=document.getElementById('pc-start'); if(o) o.style.display='none'; }")
|
||
free = pg.evaluate("""async () => {
|
||
const P = window.PROCITY, D = window.DBG;
|
||
const id = P.gigs.venueShopIds[0]; // R15: no scalar alias — the primary venue by id
|
||
D.setSegment(5);
|
||
const cover = P.gigs.coverOf(id), before = P.wallet.cash();
|
||
D.enterShop(id); await new Promise(r => setTimeout(r, 350));
|
||
return { cover, before, after: P.wallet.cash(), inRoom: !!P.interiorMode.current, band: P.gigs.bandNameOf(id) };
|
||
}""")
|
||
if free['cover'] == 0 and free['inRoom'] and free['after'] == free['before']:
|
||
OK(f"cover: free night (seed {FREE_GIG_SEED}, {free['band']}) → walked straight in, wallet untouched (${free['after']})")
|
||
else:
|
||
FAIL(f"cover: free-night entry wrong: {free}")
|
||
if errs: FAIL(f"free night: {len(errs)} console error(s); first: {errs[0][:140]}")
|
||
finally:
|
||
b.close()
|
||
|
||
# 5) ?noassets=1 + ?gigs=1 — still silent-and-fine: zero audio/band-file fetches, gig still happens
|
||
b, pg, errs = new_page(p)
|
||
reqs = []
|
||
pg.on('request', lambda r: reqs.append(r.url)
|
||
if ('/assets/audio/' in r.url or r.url.endswith('.ogg') or r.url.endswith('.m4a')
|
||
or 'custom_bands' in r.url or r.url.endswith('.glb')) else None)
|
||
try:
|
||
boot(pg, 'noassets=1')
|
||
pg.keyboard.press('Space')
|
||
na = pg.evaluate("""async () => {
|
||
const P = window.PROCITY, D = window.DBG;
|
||
const id = P.gigs.venueShopIds[0]; // R15: no scalar alias — the primary venue by id
|
||
D.setSegment(5);
|
||
D.enterShop(id); await new Promise(r => setTimeout(r, 450));
|
||
const info = P.interiorMode.crewInfo;
|
||
return { state: P.gigs.stateOf(id), inRoom: !!P.interiorMode.current, band: info && info.band,
|
||
crowd: info && info.crowd, gigs: (P.plan.gigs || []).length,
|
||
bandName: P.gigs.bandNameOf(id), audio: P.audio && P.audio.state.manifest };
|
||
}""")
|
||
if na['inRoom'] and na['band'] == 4 and na['crowd'] > 0:
|
||
OK(f"?noassets=1&gigs=1: the gig still happens (4-piece band, crowd {na['crowd']}, placeholders)")
|
||
else:
|
||
FAIL(f"?noassets=1&gigs=1: no gig without assets: {na}")
|
||
if len(reqs) == 0:
|
||
OK(f"?noassets=1&gigs=1: 0 audio/GLB/band-file fetches (manifest={na['audio']}, bands from the generator)")
|
||
else:
|
||
FAIL(f"?noassets=1&gigs=1: {len(reqs)} asset fetch(es) leaked; first {reqs[0]}")
|
||
if errs: FAIL(f"?noassets=1&gigs=1: {len(errs)} console error(s); first: {errs[0][:140]}")
|
||
else: OK('?noassets=1&gigs=1: 0 console errors (silent-and-happy)')
|
||
finally:
|
||
b.close()
|
||
|
||
|
||
GODVERSE_TOWN = 'redhill_godverse'
|
||
GODVERSE_SHOP = 3962749 # Monster Robot Party, 147 Musgrave Rd, Red Hill QLD — the real shop
|
||
GODVERSE_BASE = f'assets/stock_godverse/{GODVERSE_SHOP}/'
|
||
|
||
|
||
def smoke_real_crate(p):
|
||
"""R24 — THE REAL CRATE, gated so it cannot pass without touching its subject (the vacuous-gate law).
|
||
|
||
R23's #7a would have gone green on "real covers render" while the room held the generic v2 pack and
|
||
stock_godverse/ was never fetched once. R24's brief then specified id equality — which ALSO cannot
|
||
discriminate, because both packs number items positionally (rec_0000…), so all 120 of the atlas's ids
|
||
are generic-pack ids too. Presence proves nothing; slot numbers prove nothing. This smoke asserts the
|
||
two things a wrong pack cannot fake:
|
||
1. TITLE equality — the room's pack titles are the atlas's (0 of them exist in the generic pack).
|
||
2. TEXTURE provenance — the URL the GPU samples IS the shop's atlas file.
|
||
Plus the rungs either side: a keyed shop with no atlas falls SOFT to parody, and a plain town is
|
||
untouched. Subject absent (no town/atlas) ⇒ explicit SKIP with the reason, never a quiet pass."""
|
||
head(f'SMOKE: the real crate (R24 — {GODVERSE_TOWN} · Monster Robot Party · per-shop atlas identity)')
|
||
b, pg, errs = new_page(p)
|
||
missing = [] # atlas probes that 404 — the fail-soft path, counted not hand-waved
|
||
pg.on('response', lambda r: missing.append(r.url) if (r.status == 404 and 'stock_godverse/' in r.url) else None)
|
||
try:
|
||
pg.goto(f'{HOST}/assets/towns/{GODVERSE_TOWN}.json')
|
||
if 'godverse+osm' not in pg.content():
|
||
print(f'· SKIP: {GODVERSE_TOWN} cache absent or not godverse+osm — subject absent, nothing asserted (Lane G/E)')
|
||
return
|
||
boot(pg, f'plansrc=osm&town={GODVERSE_TOWN}&stock=real&dig=1')
|
||
res = pg.evaluate("""async ({shop, base}) => {
|
||
const D = window.DBG, P = window.PROCITY;
|
||
const idxR = await fetch(base + 'stock_record_index.json');
|
||
if (!idxR.ok) return { skip: 'atlas absent for ' + shop };
|
||
const atlas = new Set(((await idxR.json()).items || []).map(i => i.title));
|
||
const town = new Set((((await (await fetch('assets/models/stock_record_index.json')).json()).items) || [])
|
||
.map(i => i.title));
|
||
const entered = D.enterShop(shop);
|
||
await new Promise(r => setTimeout(r, 1400));
|
||
const si = D.stockInfo();
|
||
if (!si || si.error) return { fail: 'no room: ' + JSON.stringify(si) };
|
||
// a keyed shop with NO atlas must fall soft to parody, not inherit this crate
|
||
const other = (P.plan.shops || []).find(s => s.godverseShopId && s.godverseShopId !== shop);
|
||
let soft = null;
|
||
// 'g:<id>' — NOT the bare number: plan ids and godverse ids share a numeric space, so a
|
||
// bare id can silently enter a different shop and pass this assertion for the wrong reason.
|
||
if (other) { D.exitShop(); await new Promise(r => setTimeout(r, 300));
|
||
D.enterShop('g:' + other.godverseShopId); await new Promise(r => setTimeout(r, 900));
|
||
const o = D.stockInfo();
|
||
soft = { name: o.name, gid: o.godverseShopId, want: other.godverseShopId,
|
||
packItems: o.packItems }; }
|
||
return {
|
||
entered, base: si.base, name: si.name, gid: si.godverseShopId,
|
||
inAtlas: si.packTitles.filter(t => atlas.has(t)).length,
|
||
inTown: si.packTitles.filter(t => town.has(t)).length,
|
||
packItems: si.packItems,
|
||
fromAtlasTex: si.texUrls.filter(u => u.includes(base)).length,
|
||
fromTownTex: si.texUrls.filter(u => u.includes('assets/models/stock_')).length,
|
||
soft,
|
||
};
|
||
}""", {'shop': GODVERSE_SHOP, 'base': GODVERSE_BASE})
|
||
if res.get('skip'):
|
||
print(f"· SKIP: {res['skip']} — subject absent, nothing asserted (Lane G)"); return
|
||
if res.get('fail'):
|
||
FAIL(f"the crate: {res['fail']}"); return
|
||
print(f" room: {res['name']} (godverse {res['gid']}) → base {res['base']}")
|
||
OK(f"resolves its OWN base ({res['base']})") if res['base'] == GODVERSE_BASE else FAIL(f"base is {res['base']}, expected {GODVERSE_BASE}")
|
||
OK(f"TITLE equality: {res['inAtlas']}/{res['packItems']} titles are the shop's own atlas") \
|
||
if (res['packItems'] > 0 and res['inAtlas'] == res['packItems']) \
|
||
else FAIL(f"TITLE equality: only {res['inAtlas']}/{res['packItems']} titles are the shop's atlas")
|
||
OK(f"TITLE equality: 0/{res['packItems']} titles from the generic pack — the crate is not the v2 pack") \
|
||
if res['inTown'] == 0 \
|
||
else FAIL(f"TITLE equality: {res['inTown']}/{res['packItems']} titles came from the GENERIC pack")
|
||
OK(f"TEXTURE provenance: {res['fromAtlasTex']} tex from the shop's atlas, {res['fromTownTex']} generic — the pixels' own file") \
|
||
if (res['fromAtlasTex'] > 0 and res['fromTownTex'] == 0) \
|
||
else FAIL(f"TEXTURE provenance: {res['fromAtlasTex']} atlas / {res['fromTownTex']} generic — the GPU is sampling the wrong file")
|
||
if res.get('soft') is not None:
|
||
sf = res['soft']
|
||
# the subject must BE a keyed shop, or this assertion proves nothing (it passed on an
|
||
# unkeyed cafe first time round — a vacuous pass inside the anti-vacuous-pass gate)
|
||
if sf.get('gid') != sf.get('want'):
|
||
FAIL(f"soft fall: entered the WRONG shop — wanted godverse {sf.get('want')}, got {sf.get('gid')} (\"{sf['name']}\")")
|
||
elif sf['packItems'] == 0:
|
||
OK(f"soft fall: keyed-but-atlas-less \"{sf['name']}\" (godverse {sf['gid']}) → parody, not this crate")
|
||
else:
|
||
FAIL(f"soft fall BROKEN: \"{sf['name']}\" inherited {sf['packItems']} items")
|
||
# Console errors: the ONLY tolerated ones are the atlas probes that 404 — that IS the fail-soft
|
||
# path (a keyed shop with no atlas → null → parody), and it is counted and named here, never
|
||
# waved through. Any other error fails. The 404 noise itself is a real cost of probing blind;
|
||
# the fix is an atlas index in G's namespace so we never ask for what isn't there (F, R24 §24).
|
||
probes = len(missing)
|
||
if probes and len(errs) == probes:
|
||
print(f' · {probes} atlas-probe 404(s) — the fail-soft path, expected; 0 other console errors')
|
||
print(f' (cost of probing blind; fix = an atlas index so we never probe an absent shop)')
|
||
elif errs:
|
||
FAIL(f'the crate: {len(errs)} console error(s), {probes} explained as atlas probes; first: {errs[0][:120]}')
|
||
else:
|
||
print(' · 0 page errors')
|
||
finally:
|
||
b.close()
|
||
|
||
|
||
def smoke_continuity(p):
|
||
"""R15 identity continuity (ledger #3): the ped who queued/surged into a venue turns up in its crowd.
|
||
Exercises F's real relay end-to-end — VenueQueue.admitOne() → onAdmit → citizens.recordVenueEntry →
|
||
tonightRoster(id) → GigCrew.spawn({roster}) → the front crowd slots carry those identities. Asserts the
|
||
seam on identities (crewInfo.fromRoster + member.pedIndex ∈ tonightRoster), the invariant crowd ⊇
|
||
roster ∩ cap, and that an EMPTY roster injects nobody (D's byte-identical law, proven here on the shell
|
||
side; D's sim soak proves the sim side). Also ledger #5 — reports SETTLED tris (after the async
|
||
instrument GLBs land), not the pre-load number C caught the R13 smoke reporting."""
|
||
head('SMOKE: continuity (R15 — the ped you followed is in the crowd · empty-roster clean · settled tris)')
|
||
if not gigs_landed(p):
|
||
print('· gigs=1 not landed (no plan.gigs) — skipping (Lane A)')
|
||
return
|
||
b, pg, errs = new_page(p)
|
||
try:
|
||
boot(pg, '')
|
||
pg.keyboard.press('Space')
|
||
res = pg.evaluate("""async () => {
|
||
const P = window.PROCITY, D = window.DBG;
|
||
const id = P.gigs.venueShopIds[0];
|
||
const rosterOf = () => (P.citizens.tonightRoster(id) || []).map(e => e.pedIndex);
|
||
// 1) EMPTY-ROSTER PATH — enter before any admit; crew must inject only what's in the roster
|
||
// (usually 0). This is the shell side of D's "empty roster = byte-identical to R13" law.
|
||
D.setSegment(5); // NIGHT — the gig is on
|
||
const rosterAtEmpty = rosterOf();
|
||
D.enterShop(id); await new Promise(r => setTimeout(r, 550));
|
||
const ei = P.interiorMode.crewInfo;
|
||
const emptyFromRoster = ei ? ei.fromRoster : -1, emptyCrowd = ei ? ei.crowd : 0;
|
||
D.exitShop(); await new Promise(r => setTimeout(r, 250));
|
||
// 2) POPULATED PATH — let the street loop spawn the queue, then admit a few EXPLICITLY (rAF
|
||
// auto-drain is throttled in automation, per B's note). Each admit fires onAdmit → recordVenueEntry.
|
||
for (let i = 0; i < 8; i++) await new Promise(r => requestAnimationFrame(r));
|
||
const q = P.venueQueues && P.venueQueues.get(id);
|
||
const admitted = [];
|
||
if (q) for (let i = 0; i < 3 && q.count() > 0; i++) { const e = q.admitOne(); if (e) admitted.push(e.pedIndex); }
|
||
const roster = rosterOf();
|
||
D.enterShop(id); await new Promise(r => setTimeout(r, 700)); // build + crew spawn
|
||
const info = P.interiorMode.crewInfo, room = P.interiorMode.current;
|
||
const crowd = P.interiorMode.crew ? P.interiorMode.crew.members.filter(m => m.part === 'crowd') : [];
|
||
const rosterCrowd = crowd.filter(m => m.fromRoster).map(m => m.pedIndex);
|
||
// 3) SETTLED TRIS (ledger #5) — immediately after build vs after the instrument GLBs + rig atlas land
|
||
P.renderer.info.reset(); P.renderer.render(P.interiorMode.scene, P.camera);
|
||
const trisImmediate = P.renderer.info.render.triangles, draws = P.renderer.info.render.calls;
|
||
await new Promise(r => setTimeout(r, 1600));
|
||
P.renderer.info.reset(); P.renderer.render(P.interiorMode.scene, P.camera);
|
||
const trisSettled = P.renderer.info.render.triangles, drawsSettled = P.renderer.info.render.calls;
|
||
return { rosterAtEmpty, emptyFromRoster, emptyCrowd, admitted, roster, rosterCrowd,
|
||
fromRoster: info ? info.fromRoster : -1, crowd: info ? info.crowd : 0,
|
||
watchPoints: room ? room.watchPoints.length : 0,
|
||
trisImmediate, draws, trisSettled, drawsSettled };
|
||
}""")
|
||
# empty-roster path: crew injects exactly what's in the roster at that moment (0 unless surge beat us)
|
||
want_empty = min(len(res['rosterAtEmpty']), res['emptyCrowd'])
|
||
if res['emptyFromRoster'] == want_empty:
|
||
OK(f"empty roster: crew injected {res['emptyFromRoster']} from a {len(res['rosterAtEmpty'])}-entry roster "
|
||
f"(crowd {res['emptyCrowd']} — no phantom continuity, R13-identical path)")
|
||
else:
|
||
FAIL(f"empty roster: fromRoster {res['emptyFromRoster']} != roster∩cap {want_empty} (roster leaked)")
|
||
# the queue admits were recorded into tonightRoster (the onAdmit → recordVenueEntry relay)
|
||
roster, admitted, rosterCrowd = res['roster'], res['admitted'], res['rosterCrowd']
|
||
if admitted and all(a in roster for a in admitted):
|
||
OK(f"relay: {len(admitted)} queue admits → tonightRoster ({admitted} ⊆ roster of {len(roster)})")
|
||
else:
|
||
FAIL(f"relay: queue admits {admitted} not all in tonightRoster {roster} (onAdmit→recordVenueEntry broke)")
|
||
# THE SEAM — crowd ⊇ roster ∩ cap, and every roster-crowd member's pedIndex is genuinely in the roster
|
||
cap = res['crowd']
|
||
want = min(len(roster), cap)
|
||
subset_ok = set(rosterCrowd).issubset(set(roster))
|
||
if res['fromRoster'] == want and want > 0 and subset_ok and set(roster[:cap]).issubset(set(rosterCrowd)):
|
||
OK(f"continuity seam: {res['fromRoster']} of {cap} crowd came from the roster "
|
||
f"(crowd ⊇ roster∩cap ✓, all pedIndex ∈ tonightRoster ✓) — the ped you followed is in the crowd")
|
||
else:
|
||
FAIL(f"continuity seam: fromRoster {res['fromRoster']} vs roster∩cap {want}; rosterCrowd {rosterCrowd} "
|
||
f"vs roster {roster} (subset={subset_ok})")
|
||
# ledger #5 — settled-tris honesty (report both; interior gate is draws, which stays low)
|
||
OK(f"settled tris: {res['trisImmediate']:,} pre-load → {res['trisSettled']:,} settled "
|
||
f"(instrument GLBs; draws {res['draws']}→{res['drawsSettled']} ≤ {GIG_DRAWS_MAX} gate)")
|
||
if errs: FAIL(f"continuity: {len(errs)} console error(s); first: {errs[0][:140]}")
|
||
else: OK('continuity: 0 console errors')
|
||
finally:
|
||
b.close()
|
||
|
||
|
||
def main():
|
||
srv = ensure_server()
|
||
try:
|
||
with sync_playwright() as p:
|
||
flags = list(KNOWN_FLAGS)
|
||
if plansrc_landed(p):
|
||
flags.append('plansrc=osm'); print("· plansrc=osm detected as landed — included")
|
||
else:
|
||
print("· plansrc=osm not landed (Lane A) — skipping that flag")
|
||
|
||
classic_regression(p) # [R16] the frozen v2 covenant moved here: ?classic=1 → byte-identical
|
||
default_boot_gate(p) # [R16] the flip: the no-flag boot is the full experience (all four on)
|
||
interior_budget(p)
|
||
for fl in flags:
|
||
smoke(p, fl.split('=')[0], fl)
|
||
smoke(p, 'ALL-ON combo', '&'.join(flags), is_combo=True)
|
||
smoke_stock(p) # STRICT (r8) — ?stock=real real record sleeves
|
||
smoke_weather(p) # STRICT (r8) — ?weather=rain
|
||
smoke_buy(p) # STRICT (r9) — buy-v0
|
||
smoke_patronage(p) # STRICT (r9) — peds visit open shops
|
||
smoke_booktoy(p) # STRICT (r9) — book/toy real stock
|
||
smoke_presence(p) # new (warn) — interior presence (browsers)
|
||
smoke_shelfbuy(p) # new (warn) — buy-anywhere (book/toy shelves)
|
||
smoke_tram(p) # new (warn) — tram (auto-skips if not landed)
|
||
smoke_audio(p) # R11 — audio house-law (silent-happy · pre-gesture · mute · noassets · leak)
|
||
smoke_gigs(p) # R12/13 — the district gig layer (schedule · band · crowd · budget · cover · noassets)
|
||
smoke_continuity(p) # R15 — identity continuity (the ped you followed is in the crowd · settled tris)
|
||
smoke_real_crate(p) # R24 — THE REAL CRATE (per-shop atlas identity · soft fall · no-op off the path)
|
||
finally:
|
||
if srv: srv.terminate()
|
||
|
||
head('VERDICT')
|
||
print(json.dumps({'fails': len(fails), 'warns': len(warns)}, indent=0))
|
||
if fails:
|
||
print(f"\033[31m● flags_check RED\033[0m — {len(fails)} failure(s), {len(warns)} warning(s).")
|
||
for f in fails: print(' FAIL:', f)
|
||
sys.exit(1)
|
||
print(f"\033[32m● flags_check GREEN\033[0m — {len(warns)} warning(s).")
|
||
for w in warns: print(' warn:', w)
|
||
sys.exit(0)
|
||
|
||
if __name__ == '__main__':
|
||
main()
|