feedBack/tests/js/working_tuning_capability.test.js
ChrisBeWithYou 48f435408f
feat(core): working-tuning lifecycle — launch default, verified decay, idempotent re-injection + tests (working-tuning PR 9a) (#669)
Hardens the host workingTuning capability (PR 1) with the "polish & safety"
lifecycle:

- Idempotent re-injection: a second load of the module no longer replaces the live
  state with a fresh (empty) one — it early-returns once registered.
- Opt-in "launch tuning" default (setLaunchDefault/getLaunchDefault/clearLaunchDefault):
  a per-instrument, localStorage-backed seed the player can opt into ("start me in
  THIS tuning on app open"). Boot seeds from it when set, else /api/settings as before.
  Off by default — a SEED only; the live tuning still resets on restart.
- Verified decay: on song:loading the current instrument's 'verified' provenance
  decays to 'assumed' (offsets kept) — a per-string mic check is only trustworthy for
  the context it was done in, so a stale 'verified' can never suppress a needed prompt.

Adds a state-machine smoke suite (tests/js/working_tuning_capability.test.js, 12/12):
defaults, per-instrument isolation, both-directions, verified-invalidation-on-retune,
decay-on-song-load, resetToDefault, launch-default set/seed/clear, idempotent
re-injection, the change event.

The opt-in UI + the mic-verify writer land with the tuner (PR 9b).


Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 11:07:58 +02:00

145 lines
6.4 KiB
JavaScript

'use strict';
// State-machine smoke tests for the host working-tuning capability
// (static/capabilities/working-tuning.js). Runs the real module in a vm sandbox
// with stubbed window.feedBack (emit/on + capabilities), localStorage and fetch.
const { test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const vm = require('node:vm');
const SRC = fs.readFileSync(
path.join(__dirname, '..', '..', 'static', 'capabilities', 'working-tuning.js'), 'utf8');
function makeSandbox(opts) {
opts = opts || {};
const listeners = {};
const store = opts.localStorage || {};
const settings = opts.settings || { instrument: 'guitar', string_count: 6, tuning: 'Standard', reference_pitch: 440 };
const localStorage = {
getItem: (k) => (k in store ? store[k] : null),
setItem: (k, v) => { store[k] = String(v); },
removeItem: (k) => { delete store[k]; },
};
const sandbox = {
window: {
feedBack: {
capabilities: { version: 1, registerOwner() {}, registerParticipant() {} },
emit(ev, detail) { (listeners[ev] || []).slice().forEach((fn) => fn(detail)); },
on(ev, fn) { (listeners[ev] = listeners[ev] || []).push(fn); },
},
localStorage,
},
localStorage,
fetch: () => Promise.resolve({ ok: true, json: () => Promise.resolve(settings) }),
console, Promise, Date, Array, Object, JSON, Number, isFinite, setTimeout,
};
vm.createContext(sandbox);
vm.runInContext(SRC, sandbox);
return {
wt: () => sandbox.window.feedBack.workingTuning,
emit: (ev, d) => sandbox.window.feedBack.emit(ev, d),
on: (ev, fn) => sandbox.window.feedBack.on(ev, fn),
reinject: () => vm.runInContext(SRC, sandbox),
store,
};
}
const tick = () => new Promise((r) => setTimeout(r, 0));
test('defaults: an unset instrument is assumed with null offsets', () => {
const d = makeSandbox().wt().get('guitar-6');
assert.equal(d.offsets, null);
assert.equal(d.provenance, 'assumed');
});
test('set + get round-trips, per-instrument isolation (guitar vs bass)', () => {
const s = makeSandbox();
s.wt().set({ offsets: [-2, 0, 0, 0, 0, 0], stringCount: 6 }, { instrument: 'guitar-6' });
assert.deepEqual(s.wt().get('guitar-6').offsets, [-2, 0, 0, 0, 0, 0]);
assert.equal(s.wt().get('bass-4').offsets, null);
});
test('both directions: E → Drop D → back to E via set()', () => {
const s = makeSandbox();
s.wt().set({ offsets: [-2, 0, 0, 0, 0, 0] }, { instrument: 'guitar-6' });
assert.deepEqual(s.wt().get('guitar-6').offsets, [-2, 0, 0, 0, 0, 0]);
s.wt().set({ offsets: [0, 0, 0, 0, 0, 0] }, { instrument: 'guitar-6' });
assert.deepEqual(s.wt().get('guitar-6').offsets, [0, 0, 0, 0, 0, 0]); // came back
});
test('changing the tuning invalidates a prior verification', () => {
const s = makeSandbox();
s.wt().set({ offsets: [0, 0, 0, 0, 0, 0], verifiedStrings: [1, 1, 1, 1, 1, 1] }, { instrument: 'guitar-6', provenance: 'verified' });
assert.equal(s.wt().get('guitar-6').provenance, 'verified');
s.wt().set({ offsets: [-2, 0, 0, 0, 0, 0] }, { instrument: 'guitar-6' });
assert.equal(s.wt().get('guitar-6').provenance, 'assumed');
});
test('verified decays to assumed on song:loading (offsets kept)', () => {
const s = makeSandbox();
s.wt().set({ offsets: [0, 0, 0, 0, 0, 0], verifiedStrings: [1, 1, 1, 1, 1, 1] }, { instrument: 'guitar-6', provenance: 'verified' });
assert.equal(s.wt().get('guitar-6').provenance, 'verified');
s.emit('song:loading', { filename: 'x' });
assert.equal(s.wt().get('guitar-6').provenance, 'assumed');
assert.deepEqual(s.wt().get('guitar-6').offsets, [0, 0, 0, 0, 0, 0]);
});
test('resetToDefault clears back to defaults', () => {
const s = makeSandbox();
s.wt().set({ offsets: [-2, 0, 0, 0, 0, 0] }, { instrument: 'guitar-6' });
s.wt().resetToDefault('guitar-6');
assert.equal(s.wt().get('guitar-6').offsets, null);
});
test('launch default: setLaunchDefault persists + getLaunchDefault returns it', () => {
const s = makeSandbox();
s.wt().set({ offsets: [-2, 0, 0, 0, 0, 0], stringCount: 6 }, { instrument: 'guitar-6' });
s.wt().setLaunchDefault('guitar-6');
assert.deepEqual(s.wt().getLaunchDefault('guitar-6').offsets, [-2, 0, 0, 0, 0, 0]);
assert.ok('v3-working-tuning-launch-default' in s.store);
});
test('launch default is opt-in: none set → getLaunchDefault is null', () => {
assert.equal(makeSandbox().wt().getLaunchDefault('guitar-6'), null);
});
test('launch default seeds a fresh boot and wins over the raw profile', async () => {
const store = {};
const s1 = makeSandbox({ localStorage: store });
s1.wt().set({ offsets: [-2, 0, 0, 0, 0, 0], stringCount: 6 }, { instrument: 'guitar-6' });
s1.wt().setLaunchDefault('guitar-6');
// Fresh boot with the SAME localStorage; settings say Standard → launch default wins.
const s2 = makeSandbox({ localStorage: store, settings: { instrument: 'guitar', string_count: 6, tuning: 'Standard', reference_pitch: 440 } });
await tick();
assert.deepEqual(s2.wt().get('guitar-6').offsets, [-2, 0, 0, 0, 0, 0]);
assert.equal(s2.wt().get('guitar-6').source, 'launch-default');
});
test('clearLaunchDefault removes it', () => {
const s = makeSandbox();
s.wt().set({ offsets: [-2, 0, 0, 0, 0, 0], stringCount: 6 }, { instrument: 'guitar-6' });
s.wt().setLaunchDefault('guitar-6');
s.wt().clearLaunchDefault('guitar-6');
assert.equal(s.wt().getLaunchDefault('guitar-6'), null);
});
test('idempotent re-injection: a second load does NOT clobber live state', () => {
const s = makeSandbox();
s.wt().set({ offsets: [-2, 0, 0, 0, 0, 0], stringCount: 6 }, { instrument: 'guitar-6' });
s.reinject(); // run the module source again in the same context
assert.deepEqual(s.wt().get('guitar-6').offsets, [-2, 0, 0, 0, 0, 0]); // preserved
});
test('working-tuning-changed fires on set with the changed instrument', () => {
const s = makeSandbox();
const seen = [];
s.on('working-tuning-changed', (e) => seen.push(e));
s.wt().set({ offsets: [-2, 0, 0, 0, 0, 0], stringCount: 6 }, { instrument: 'guitar-6' });
assert.ok(seen.length >= 1);
const last = seen[seen.length - 1];
assert.equal(last.instrument, 'guitar');
assert.deepEqual(last.tuning.offsets, [-2, 0, 0, 0, 0, 0]);
});