Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions .claude/skills/run-virtuoso/smoke-coach-rx.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,51 @@ try {
ok(got.chosen === h.chosen && got.streak === h.streak, `hysteresis: ${h.n}`, `chosen=${got.chosen} streak=${got.streak}`);
}

// ── REGRESSION (v0.2.5): the run-end _lastEndedSession snapshot must carry
// pathway_id + practice_type. The bug: the snapshot copied `mode` but dropped
// those two, so buildCoachRx's `curPw = s.mode==='pathway' ? s.pathway_id : null`
// was ALWAYS undefined → the anti-self-prescribe pick() was a no-op (it
// re-prescribed the just-run rung) and the hysteresis bucket key collapsed to
// one shared "pathway:" bucket. The pure hooks above take an explicit ctx.curPw,
// so they could not catch it — this drives a REAL pathway run and inspects the
// actual _lastEndedSession → curPw seam.
console.log("\n-- run-end snapshot → curPw seam (drives a real run) --");
const RUNG = "chromatic_warmup", SIBLING = "fs_spider_adjacent";
await page.evaluate((id) => {
try { localStorage.removeItem("virtuoso.coach_focus"); } catch (_) {} // clear hysteresis so the behavioral assert is fresh
const sel = document.querySelector("#virtuoso-pathway");
sel.value = id; sel.dispatchEvent(new Event("change", { bubbles: true }));
}, RUNG);
await page.waitForTimeout(300);
// Start, hold > 2s (sessionEnd discards sub-2s blips), then Stop (session end).
await page.click("#virtuoso-play");
const started = await page.waitForSelector("#virtuoso-stop:not([disabled])", { timeout: 8000 }).then(() => true).catch(() => false);
ok(started, "real pathway run started (stop button armed)");
await page.waitForTimeout(4200); // clear the 2s duration gate even with a count-in
await page.click("#virtuoso-stop");
Comment on lines +160 to +164

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Guard the stop sequence when startup fails.

ok(started, ...) records the failure, but execution still clicks Stop. If startup times out, that action can fail separately and obscure the actual root cause. Guard the wait/stop sequence with started or fail fast with a focused startup error.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.claude/skills/run-virtuoso/smoke-coach-rx.mjs around lines 160 - 164, Guard
the stop sequence after the `#virtuoso-play` startup in the smoke test using the
started result. Only wait for the duration gate and click `#virtuoso-stop` when
startup succeeds; otherwise fail fast with a focused startup failure while
preserving the existing ok assertion.

await page.waitForTimeout(400);

const snap = await page.evaluate(() => {
const s = window.__virtuosoCoach.lastEndedSession();
return s ? { mode: s.mode, pathway_id: s.pathway_id, practice_type: s.practice_type } : null;
});
ok(!!snap, "a >2s pathway run produced a _lastEndedSession snapshot");
ok(!!snap && snap.mode === "pathway", "snapshot.mode === 'pathway'", snap && `mode=${snap.mode}`);
ok(!!snap && snap.pathway_id === RUNG, "snapshot carries pathway_id (the dropped field)", snap && `pathway_id=${snap.pathway_id}`);
ok(!!snap && !!snap.practice_type, "snapshot carries practice_type (the other dropped field)", snap && `practice_type=${snap.practice_type}`);

// Behavioral: feed the REAL snapshot + a finger fault through the REAL buildCoachRx.
// The whole point of the fix — it must NOT re-prescribe the rung just run.
const rx = await page.evaluate(() => {
const s = window.__virtuosoCoach.lastEndedSession();
if (!s) return { err: "no snapshot" };
const info = { heat: { missTotal: 5, fgMiss: { 4: 4 }, transMiss: {}, opens: [] } }; // dropped-pinky fault
const r = window.__virtuosoCoach.buildCoachRx(s, { judgedPassed: 10 }, info);
return { pathwayId: r ? r.pathwayId : null };
});
ok(rx.pathwayId && rx.pathwayId !== RUNG, "coach does NOT re-prescribe the just-run rung (curPw honored)", `→ ${rx.pathwayId}`);
ok(rx.pathwayId === SIBLING, "a finger fault on chromatic_warmup falls to its sibling", `→ ${rx.pathwayId} (want ${SIBLING})`);

ok(pageErrs.length === 0, "no uncaught page errors", pageErrs.join(" | "));
console.log(`\n${fails === 0 ? "PASS" : "FAIL"} coach prescription: ${fails} failure(s)`);
process.exit(fails ? 1 : 0);
Expand Down
2 changes: 1 addition & 1 deletion plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "virtuoso",
"name": "Virtuoso",
"version": "0.2.5",
"version": "0.2.6",
"description": "Practice studio for guitar & bass — scale, technique, and rhythm drills, timed workouts, and jam backing that teach skills you take off the screen.",
"category": "practice",
"icon": "assets/thumb.png",
Expand Down
8 changes: 5 additions & 3 deletions screen.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
// a plugin's own version into its screen (note_detect hardcodes `_ND_VERSION`
// the same way), so this is the display mirror of plugin.json's "version".
// BUMP THIS WHENEVER plugin.json's version changes (release checklist).
const VIRTUOSO_VERSION = '0.2.5';
const VIRTUOSO_VERSION = '0.2.6';

// ===========================================================================
// §1 · CONSTANTS & MUSIC-THEORY DATA
Expand Down Expand Up @@ -17753,8 +17753,10 @@
return null;
}
// Debug/test hook: the pure mappings, exposed for the smoke suite (immune to the
// "smoke mocks the verifier" blind spot — they take fixtures directly).
if (typeof window !== 'undefined') window.__virtuosoCoach = { coachRxFor, coachRxCandidates, applyCoachHysteresis, coachRxHtml, buildPocketDiagnosis };
// "smoke mocks the verifier" blind spot — they take fixtures directly). `buildCoachRx`
// + `lastEndedSession` additionally let the suite drive a REAL run and assert the
// _lastEndedSession → curPw snapshot seam (the field-drop the pure hooks couldn't catch).
if (typeof window !== 'undefined') window.__virtuosoCoach = { coachRxFor, coachRxCandidates, applyCoachHysteresis, coachRxHtml, buildPocketDiagnosis, buildCoachRx, lastEndedSession: () => _lastEndedSession };
// ── J-3 end-of-jam reflection (warm, NO score — mirror not judge) ───────────
// A deliberate Jam stop opens the SAME modal shell as the results card, but
// with descriptive content only: time jammed, how many notes, the tones you
Expand Down
Loading