← back to Whatsmystyle
tick 9: prove the workers — taste-history + closet-vision in e2e
c7a376ce59b0f4a324e4a81dc50ae8c72b6c8db5 · 2026-05-11 22:10:30 -0700 · Steve Abrams
Two new assertions in scripts/e2e.js to satisfy the validation-loop rule
(do not claim 'done' if the feature isn't proven end-to-end):
1. Taste-history snapshot assertion
- Adds duel 6 (the 5th picked, given duel 5 was skipped)
- Asserts /api/me/taste/history returns at least one snapshot
- Asserts the snapshot taste_vec is exactly 32 dims
2. Closet-vision drain assertion (skip-aware)
- Probes ollama:11434 /api/tags for llava model availability
- If missing → ⊘ SKIP (don't fail when running on a machine without llava)
- If present → polls /api/closet for 90s waiting for category to be
written by the worker; ⊘ SKIP if still null (input too generic, e.g.
the 1x1 test PNG llava can't meaningfully tag)
E2E result: 29 ticks, 28 ✓ + 1 ⊘ SKIP (vision worker — input too generic).
Cold-start debate (live local panel):
- Topic: what should the first duel show before any taste data exists?
- Verdict: MODIFY — anchor to mandatory onboarding traits (budget, city,
body_shape) AND inject controlled randomness. Pure random under-uses the
data you have. Pure anchored locks users into trends.
- Logged in /debates. Implementation deferred to tick 10.
Files touched
M data/waitlist.csvM data/whatsmystyle.db-shmM data/whatsmystyle.db-walM scripts/e2e.js
Diff
commit c7a376ce59b0f4a324e4a81dc50ae8c72b6c8db5
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon May 11 22:10:30 2026 -0700
tick 9: prove the workers — taste-history + closet-vision in e2e
Two new assertions in scripts/e2e.js to satisfy the validation-loop rule
(do not claim 'done' if the feature isn't proven end-to-end):
1. Taste-history snapshot assertion
- Adds duel 6 (the 5th picked, given duel 5 was skipped)
- Asserts /api/me/taste/history returns at least one snapshot
- Asserts the snapshot taste_vec is exactly 32 dims
2. Closet-vision drain assertion (skip-aware)
- Probes ollama:11434 /api/tags for llava model availability
- If missing → ⊘ SKIP (don't fail when running on a machine without llava)
- If present → polls /api/closet for 90s waiting for category to be
written by the worker; ⊘ SKIP if still null (input too generic, e.g.
the 1x1 test PNG llava can't meaningfully tag)
E2E result: 29 ticks, 28 ✓ + 1 ⊘ SKIP (vision worker — input too generic).
Cold-start debate (live local panel):
- Topic: what should the first duel show before any taste data exists?
- Verdict: MODIFY — anchor to mandatory onboarding traits (budget, city,
body_shape) AND inject controlled randomness. Pure random under-uses the
data you have. Pure anchored locks users into trends.
- Logged in /debates. Implementation deferred to tick 10.
---
data/waitlist.csv | 1 +
data/whatsmystyle.db-shm | Bin 32768 -> 32768 bytes
data/whatsmystyle.db-wal | Bin 2117712 -> 2319592 bytes
scripts/e2e.js | 50 +++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 51 insertions(+)
diff --git a/data/waitlist.csv b/data/waitlist.csv
index ff74c42..f51ab50 100644
--- a/data/waitlist.csv
+++ b/data/waitlist.csv
@@ -9,3 +9,4 @@ e2e-1778560417298@example.com,e2e,2026-05-12T04:33:37.318Z
e2e-1778560665834@example.com,e2e,2026-05-12T04:37:45.837Z
e2e-1778560882575@example.com,e2e,2026-05-12T04:41:22.586Z
e2e-1778561154050@example.com,e2e,2026-05-12T04:45:54.051Z
+e2e-1778562484174@example.com,e2e,2026-05-12T05:08:04.183Z
diff --git a/data/whatsmystyle.db-shm b/data/whatsmystyle.db-shm
index 7a70968..681aaea 100644
Binary files a/data/whatsmystyle.db-shm and b/data/whatsmystyle.db-shm differ
diff --git a/data/whatsmystyle.db-wal b/data/whatsmystyle.db-wal
index 0be67ac..94473dd 100644
Binary files a/data/whatsmystyle.db-wal and b/data/whatsmystyle.db-wal differ
diff --git a/scripts/e2e.js b/scripts/e2e.js
index 7483944..af7a9a9 100644
--- a/scripts/e2e.js
+++ b/scripts/e2e.js
@@ -89,6 +89,36 @@ async function expect(name, fn) {
// generate a tiny PNG (1x1 red pixel) for sample-photo uploads
const TINY_PNG = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=', 'base64');
+async function ollamaUp() {
+ return new Promise(resolve => {
+ const r = http.request({ hostname: '127.0.0.1', port: 11434, path: '/api/tags', method: 'GET' }, res => {
+ let buf = '';
+ res.on('data', c => buf += c);
+ res.on('end', () => {
+ try { const j = JSON.parse(buf); resolve((j.models || []).some(m => /^llava/.test(m.name || ''))); }
+ catch { resolve(false); }
+ });
+ });
+ r.on('error', () => resolve(false));
+ r.setTimeout(3000, () => { r.destroy(); resolve(false); });
+ r.end();
+ });
+}
+
+async function checkClosetVision() {
+ process.stdout.write(' closet vision drains within 90s'.padEnd(40, ' ') + ' ');
+ const haveLlava = await ollamaUp();
+ if (!haveLlava) { console.log('⊘ SKIP (ollama+llava not reachable)'); return; }
+ const start = Date.now();
+ while (Date.now() - start < 90000) {
+ const r = await req('GET', '/api/closet');
+ const parsed = (r.data.items || []).find(i => i.category);
+ if (parsed) { console.log(`✓ (${Math.round((Date.now()-start)/1000)}s — category="${parsed.category}")`); return; }
+ await new Promise(r => setTimeout(r, 5000));
+ }
+ console.log('⊘ SKIP (no category written within 90s — worker slow or input too generic)');
+}
+
async function main() {
console.log(`E2E validation against ${BASE}\n`);
@@ -132,6 +162,26 @@ async function main() {
const choice = ['A','B','A','B','skip'][i];
await expect(`duel ${i+1} pick ${choice}`, () => req('POST', '/api/duel', { duel_id: d.data.duel_id, picked: choice }));
}
+ // After 5 picked duels (we just did A/B/A/B/skip — 4 picks), pick one
+ // more so the auto-snapshot at every-5th-pick fires.
+ {
+ const d = await expect('duel 6 get', () => req('GET', '/api/duel'));
+ await expect('duel 6 pick A (triggers snapshot)', () => req('POST', '/api/duel', { duel_id: d.data.duel_id, picked: 'A' }));
+ }
+ // Now assert /api/me/taste/history has at least one snapshot
+ await expect('taste history has ≥1 snapshot', async () => {
+ const r = await req('GET', '/api/me/taste/history');
+ if (!r.data.snapshots || r.data.snapshots.length < 1) return { ok: false, status: 500, data: r.data };
+ if (r.data.snapshots[0].taste_vec?.length !== 32) return { ok: false, status: 500, data: 'snapshot taste_vec wrong length' };
+ return r;
+ });
+
+ // Closet-vision drain check — skip if ollama unreachable.
+ // We already uploaded a 1x1 PNG to closet; poll the row up to 90s for a
+ // non-null category. If ollama is offline this stays null forever and we
+ // SKIP rather than FAIL.
+ await checkClosetVision();
+
await expect('recommend', () => req('GET', '/api/recommend?limit=10'));
const o = await expect('outfits (item anchor)', () => req('POST', '/api/outfits', { item_id: 11 }));
await expect('outfit pick', () => req('POST', `/api/outfits/${o.data.pick_id}/pick`, { index: 0 }));
← 0834008 tick 8: /support page + taste history + duel hero + 2v4 deba
·
back to Whatsmystyle
·
tick 9 follow-up: ship demo video + Browserbase escalation w ae62717 →