mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-13 20:21:40 +00:00
feat(tuner): mic-verify — promote working tuning assumed→verified via a per-string check (working-tuning PR 9b) (#670)
* feat(tuner): mic-verify — promote the working tuning assumed→verified via a per-string check (working-tuning PR 9b) Adds the choreographed per-string mic verification the design reserved for 'verified' provenance (audio-engine's honesty rule — nothing else may claim it): the player plays each string, and once every one reads in tune (±6 cents) and holds stable for 8 frames, the tuner stamps the working tuning provenance:'verified' + verifiedStrings via workingTuning.set. - screen.js: a pure verify state machine (verifyStart/verifyFeed/verifyCancel/ verifyState, exposed on the tuner API) + the set-verified writer; cancels on close. - ui.js: updateUI feeds each processed frame (matched string + cents) into the session; a "Verify tuning" button + per-string progress + status, shown for a selected (non-free) tuning. Pairs with the 9a lifecycle: a 'verified' decays back to 'assumed' on the next song load, so mic-verify is a per-session confidence boost, never a sticky claim. Tests: tests/js/tuner_auto_open.test.js +4 (all-strings->verified, out-of-tune never completes, streak resets on drift, API exposed / only it claims verified) — 33/33. The state machine is headless-verified with synthetic frames; the real per-string mic detection + the button flow need an on-device pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF * fix(tuner): mic-verify writes the confirmed tuning + no clobber + stricter streak (PR #670 review) Review fixes for mic-verify (working-tuning PR 9b): - 'verified' could attach to STALE offsets: _publishVerified stamped provenance without writing offsets, so the slot's pre-tuning offsets got marked verified. verifyStart(targets, offsets) now captures the confirmed tuning's offsets, and _publishVerified writes offsets + stringCount + instrument + referencePitch + verifiedStrings ATOMICALLY with provenance:'verified' into the selected slot (and refuses to stamp verified with no concrete offsets). - The assumed publish-on-clear immediately clobbered a just-earned 'verified': disable() now skips it when a mic-verify wrote verified this session (_verifiedPublished). - The per-string streak could accumulate across silence / wrong-string frames. verifyFeed now requires CONSECUTIVE in-tune frames: the one confirmed string advances, every other unfinished string resets each frame. - A mid-verify tuning change (song switch) left stale captured offsets; verify is now cancelled in _syncCurrentTuning when the song tuning changes. Tests: verify writes the confirmed offsets (not stale); source-guard for the no-clobber path. 47 tuner + 77 tuner/capability tests green. Codex-reviewed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: byrongamatos <xasiklas@gmail.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
byrongamatos
parent
115c96a3f0
commit
2910a3c8cb
+87
-2
@@ -490,6 +490,12 @@
|
|||||||
const sc = (typeof window.feedBack?.effectiveStringCount === 'function')
|
const sc = (typeof window.feedBack?.effectiveStringCount === 'function')
|
||||||
? window.feedBack.effectiveStringCount(songInfo.tuning, ctx)
|
? window.feedBack.effectiveStringCount(songInfo.tuning, ctx)
|
||||||
: (songInfo.stringCount || songInfo.tuning.length);
|
: (songInfo.stringCount || songInfo.tuning.length);
|
||||||
|
// A tuning change invalidates an in-flight mic-verify — its captured targets
|
||||||
|
// and offsets are now stale, so it must not complete against the old tuning.
|
||||||
|
if (_verify && String(songInfo.tuning.slice(0, sc)) !== String(_verify.offsets)) {
|
||||||
|
verifyCancel();
|
||||||
|
_tunerUIApi?.resetVerify?.();
|
||||||
|
}
|
||||||
_state.currentSongOffsets = songInfo.tuning.slice(0, sc);
|
_state.currentSongOffsets = songInfo.tuning.slice(0, sc);
|
||||||
_state.currentSongIsBass = isBass;
|
_state.currentSongIsBass = isBass;
|
||||||
_state.currentSongStringCount = sc;
|
_state.currentSongStringCount = sc;
|
||||||
@@ -725,6 +731,8 @@
|
|||||||
_state.autoOpened = false;
|
_state.autoOpened = false;
|
||||||
_releaseGate(); // dismissing a gated auto-open releases playback (it starts now)
|
_releaseGate(); // dismissing a gated auto-open releases playback (it starts now)
|
||||||
_state.manualTargetFreq = null;
|
_state.manualTargetFreq = null;
|
||||||
|
verifyCancel(); // a running mic-verify ends when the panel closes
|
||||||
|
_tunerUIApi?.resetVerify?.();
|
||||||
if (_outsideClickClose) { document.removeEventListener('click', _outsideClickClose); _outsideClickClose = null; }
|
if (_outsideClickClose) { document.removeEventListener('click', _outsideClickClose); _outsideClickClose = null; }
|
||||||
if (_state.activeViz) { _state.activeViz.destroy(); _state.activeViz = null; }
|
if (_state.activeViz) { _state.activeViz.destroy(); _state.activeViz = null; }
|
||||||
if (_state.uiContainer) { _state.uiContainer.classList.add('hidden'); _state.uiContainer.classList.remove('flex'); }
|
if (_state.uiContainer) { _state.uiContainer.classList.add('hidden'); _state.uiContainer.classList.remove('flex'); }
|
||||||
@@ -746,10 +754,13 @@
|
|||||||
_autoOpenDismissedSessionKey = _autoOpenSessionKey(songInfo);
|
_autoOpenDismissedSessionKey = _autoOpenSessionKey(songInfo);
|
||||||
// Clearing an auto-opened tuner = the player tuned to this song:
|
// Clearing an auto-opened tuner = the player tuned to this song:
|
||||||
// publish the song's tuning as their instrument's live working tuning
|
// publish the song's tuning as their instrument's live working tuning
|
||||||
// so coverage stops nagging for it (and prompts on the way back).
|
// so coverage stops nagging for it (and prompts on the way back). But
|
||||||
if (wasAutoOpened) _publishWorkingTuning(songInfo);
|
// if a mic-verify already wrote 'verified' this session, a plain
|
||||||
|
// 'assumed' publish would immediately clobber it — leave it verified.
|
||||||
|
if (wasAutoOpened && !_verifiedPublished) _publishWorkingTuning(songInfo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
_verifiedPublished = false; // consumed — fresh for the next tuner session
|
||||||
}
|
}
|
||||||
|
|
||||||
window.tuner = {
|
window.tuner = {
|
||||||
@@ -793,6 +804,76 @@
|
|||||||
_installAutoOpenListeners();
|
_installAutoOpenListeners();
|
||||||
}).catch(e => console.error(e));
|
}).catch(e => console.error(e));
|
||||||
_installAutoOpenListeners();
|
_installAutoOpenListeners();
|
||||||
|
|
||||||
|
// ── Mic-verify (working-tuning PR 9b) ──────────────────────────────────────
|
||||||
|
// A choreographed per-string check that promotes the current working tuning
|
||||||
|
// from 'assumed' to 'verified' — the ONLY thing that may ever claim 'verified'
|
||||||
|
// (audio-engine's honesty rule). The player plays each string; once every one
|
||||||
|
// reads in-tune (±VERIFY_TOL_CENTS) and holds for VERIFY_STABLE frames we stamp
|
||||||
|
// provenance:'verified' + verifiedStrings. Cancels on tuner close / tuning change.
|
||||||
|
const VERIFY_TOL_CENTS = 6;
|
||||||
|
const VERIFY_STABLE = 8;
|
||||||
|
let _verify = null;
|
||||||
|
let _verifiedPublished = false; // set when a mic-verify wrote 'verified' this session
|
||||||
|
|
||||||
|
function verifyState() {
|
||||||
|
if (!_verify) return null;
|
||||||
|
return {
|
||||||
|
complete: _verify.complete,
|
||||||
|
done: _verify.targets.map((t) => t.done),
|
||||||
|
remaining: _verify.targets.filter((t) => !t.done).length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Start a verify session for `targets` (freqs; defaults to the selected tuning).
|
||||||
|
// Captures the tuning's OFFSETS now (explicit arg, else the current song's) so that
|
||||||
|
// when it completes we stamp 'verified' onto the exact tuning that was confirmed.
|
||||||
|
function verifyStart(targets, offsets) {
|
||||||
|
const freqs = (Array.isArray(targets) && targets.length) ? targets : _state.selectedTuning;
|
||||||
|
if (!Array.isArray(freqs) || !freqs.length) return null;
|
||||||
|
const offs = (Array.isArray(offsets) && offsets.length) ? offsets.slice()
|
||||||
|
: (Array.isArray(_state.currentSongOffsets) ? _state.currentSongOffsets.slice() : null);
|
||||||
|
_verify = { targets: freqs.map((f) => ({ freq: f, streak: 0, done: false })), complete: false, offsets: offs };
|
||||||
|
_verifiedPublished = false;
|
||||||
|
return verifyState();
|
||||||
|
}
|
||||||
|
function verifyCancel() { _verify = null; }
|
||||||
|
// Feed one processed frame (its matched target freq + cents-off). Requires
|
||||||
|
// CONSECUTIVE in-tune frames per string: the one confirmed string advances, and
|
||||||
|
// every other not-yet-done string's streak resets — so a run can't accumulate across
|
||||||
|
// silence / wrong-string / out-of-tune frames. Completes + stamps 'verified' when all pass.
|
||||||
|
function verifyFeed(targetFreq, cents) {
|
||||||
|
if (!_verify || _verify.complete) return verifyState();
|
||||||
|
let hit = null;
|
||||||
|
if (targetFreq != null) {
|
||||||
|
const t = _verify.targets.find((x) => Math.abs(x.freq - targetFreq) < 0.5);
|
||||||
|
if (t && !t.done && isFinite(cents) && Math.abs(cents) <= VERIFY_TOL_CENTS) hit = t;
|
||||||
|
}
|
||||||
|
for (const t of _verify.targets) {
|
||||||
|
if (t.done) continue;
|
||||||
|
if (t === hit) { if (++t.streak >= VERIFY_STABLE) t.done = true; }
|
||||||
|
else t.streak = 0;
|
||||||
|
}
|
||||||
|
if (_verify.targets.every((x) => x.done)) {
|
||||||
|
_verify.complete = true;
|
||||||
|
_publishVerified();
|
||||||
|
}
|
||||||
|
return verifyState();
|
||||||
|
}
|
||||||
|
// Promote the working tuning to 'verified'. Write the CONFIRMED tuning's offsets
|
||||||
|
// atomically with provenance + verifiedStrings (into the selected instrument's slot),
|
||||||
|
// so 'verified' can never attach to stale offsets the slot happened to hold.
|
||||||
|
function _publishVerified() {
|
||||||
|
const wt = window.feedBack && window.feedBack.workingTuning;
|
||||||
|
if (!wt || typeof wt.set !== 'function') return;
|
||||||
|
const offsets = (_verify && Array.isArray(_verify.offsets)) ? _verify.offsets.slice() : null;
|
||||||
|
if (!offsets || !offsets.length) return; // nothing concrete to claim verified
|
||||||
|
const sel = _state._playerSelected;
|
||||||
|
const next = { offsets: offsets, stringCount: offsets.length, verifiedStrings: offsets.map(() => true) };
|
||||||
|
if (sel && sel.key) { next.instrument = sel.isBass ? 'bass' : 'guitar'; next.referencePitch = sel.refPitch; }
|
||||||
|
const opts = (sel && sel.key) ? { instrument: sel.key, provenance: 'verified' } : { provenance: 'verified' };
|
||||||
|
try { wt.set(next, opts); _verifiedPublished = true; } catch (_) { /* noop */ }
|
||||||
|
}
|
||||||
|
|
||||||
window._tunerAutoOpen = {
|
window._tunerAutoOpen = {
|
||||||
tuningIdentityKey: _tuningIdentityKey,
|
tuningIdentityKey: _tuningIdentityKey,
|
||||||
sessionKey: _autoOpenSessionKey,
|
sessionKey: _autoOpenSessionKey,
|
||||||
@@ -801,6 +882,10 @@
|
|||||||
coverageReport: _coverageReport,
|
coverageReport: _coverageReport,
|
||||||
playerTuning: _playerTuning,
|
playerTuning: _playerTuning,
|
||||||
publishWorkingTuning: _publishWorkingTuning,
|
publishWorkingTuning: _publishWorkingTuning,
|
||||||
|
verifyStart: verifyStart,
|
||||||
|
verifyFeed: verifyFeed,
|
||||||
|
verifyCancel: verifyCancel,
|
||||||
|
verifyState: verifyState,
|
||||||
onSongLoading: _onAutoOpenSongLoadingHandler,
|
onSongLoading: _onAutoOpenSongLoadingHandler,
|
||||||
getState() {
|
getState() {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -305,6 +305,12 @@ window._tunerUI = function(state, actions) {
|
|||||||
function renderStringNotes() {
|
function renderStringNotes() {
|
||||||
if (!state.stringNoteContainer) return;
|
if (!state.stringNoteContainer) return;
|
||||||
state.stringNoteContainer.innerHTML = '';
|
state.stringNoteContainer.innerHTML = '';
|
||||||
|
// Show the mic-verify control only for a selected (non-free) tuning.
|
||||||
|
if (state.verifyRow) {
|
||||||
|
const hasTuning = !!(state.selectedTuning && state.selectedTuning.length && !state.freeTune);
|
||||||
|
state.verifyRow.classList.toggle('hidden', !hasTuning);
|
||||||
|
if (!hasTuning) resetVerifyUI();
|
||||||
|
}
|
||||||
if (!state.selectedTuning || state.selectedTuning.length === 0) {
|
if (!state.selectedTuning || state.selectedTuning.length === 0) {
|
||||||
_syncStringOrderHelp(0);
|
_syncStringOrderHelp(0);
|
||||||
return;
|
return;
|
||||||
@@ -327,6 +333,41 @@ window._tunerUI = function(state, actions) {
|
|||||||
_syncStringOrderHelp(total);
|
_syncStringOrderHelp(total);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Mic-verify UI (working-tuning PR 9b) ───────────────────────────────────
|
||||||
|
function _markVerifiedStrings(done) {
|
||||||
|
if (!state.stringNoteContainer) return;
|
||||||
|
state.stringNoteContainer.querySelectorAll('[data-freq]').forEach((btn, i) => {
|
||||||
|
btn.classList.toggle('ring-2', !!done[i]);
|
||||||
|
btn.classList.toggle('ring-emerald-400', !!done[i]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function _syncVerifyProgress(vs) {
|
||||||
|
if (!vs || !state.verifyStatus) return;
|
||||||
|
const total = vs.done.length;
|
||||||
|
const done = vs.done.filter(Boolean).length;
|
||||||
|
state.verifyStatus.classList.remove('hidden');
|
||||||
|
state.verifyStatus.textContent = vs.complete
|
||||||
|
? '✓ In tune — tuning verified'
|
||||||
|
: (done + ' of ' + total + ' strings in tune');
|
||||||
|
_markVerifiedStrings(vs.done);
|
||||||
|
if (vs.complete && state.verifyBtn) state.verifyBtn.textContent = 'Verify tuning';
|
||||||
|
}
|
||||||
|
function _startVerify() {
|
||||||
|
if (!window._tunerAutoOpen || typeof window._tunerAutoOpen.verifyStart !== 'function') return;
|
||||||
|
const vs = window._tunerAutoOpen.verifyStart();
|
||||||
|
if (!vs) return;
|
||||||
|
if (state.verifyBtn) state.verifyBtn.textContent = 'Verifying — play each string…';
|
||||||
|
_syncVerifyProgress(vs);
|
||||||
|
}
|
||||||
|
function resetVerifyUI() {
|
||||||
|
if (window._tunerAutoOpen && typeof window._tunerAutoOpen.verifyCancel === 'function') {
|
||||||
|
window._tunerAutoOpen.verifyCancel();
|
||||||
|
}
|
||||||
|
if (state.verifyBtn) state.verifyBtn.textContent = 'Verify tuning';
|
||||||
|
if (state.verifyStatus) { state.verifyStatus.classList.add('hidden'); state.verifyStatus.textContent = ''; }
|
||||||
|
_markVerifiedStrings([]);
|
||||||
|
}
|
||||||
|
|
||||||
function updateUI(result) {
|
function updateUI(result) {
|
||||||
const { smoothedFreq, rms, hasSignal } = result;
|
const { smoothedFreq, rms, hasSignal } = result;
|
||||||
const vizMode = state.manualTargetFreq ? 'manual'
|
const vizMode = state.manualTargetFreq ? 'manual'
|
||||||
@@ -387,6 +428,13 @@ window._tunerUI = function(state, actions) {
|
|||||||
if (window.feedBack && window.feedBack.emit) {
|
if (window.feedBack && window.feedBack.emit) {
|
||||||
window.feedBack.emit('tuner:frame', { note, cents, freq: displayFreq, hasSignal: true });
|
window.feedBack.emit('tuner:frame', { note, cents, freq: displayFreq, hasSignal: true });
|
||||||
}
|
}
|
||||||
|
// Mic-verify: feed the matched string + cents to a running verify session
|
||||||
|
// and reflect per-string progress on the panel.
|
||||||
|
if (!isManual && !state.freeTune && window._tunerAutoOpen
|
||||||
|
&& typeof window._tunerAutoOpen.verifyFeed === 'function') {
|
||||||
|
const vs = window._tunerAutoOpen.verifyFeed(targetFreq, Math.round(cents));
|
||||||
|
if (vs) _syncVerifyProgress(vs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateFloatingButtonVisibility() {
|
function updateFloatingButtonVisibility() {
|
||||||
@@ -635,6 +683,22 @@ window._tunerUI = function(state, actions) {
|
|||||||
state.vizContainer.className = 'w-full';
|
state.vizContainer.className = 'w-full';
|
||||||
state.uiContainer.appendChild(state.vizContainer);
|
state.uiContainer.appendChild(state.vizContainer);
|
||||||
|
|
||||||
|
// Mic-verify control (working-tuning PR 9b): play each string in tune to
|
||||||
|
// confirm your tuning — promotes 'assumed' → 'verified'. Shown only for a
|
||||||
|
// selected (non-free) tuning; visibility managed in renderStringNotes().
|
||||||
|
state.verifyRow = document.createElement('div');
|
||||||
|
state.verifyRow.className = 'w-full mt-2 hidden';
|
||||||
|
state.verifyBtn = document.createElement('button');
|
||||||
|
state.verifyBtn.className = 'tuner-verify-btn w-full text-[11px] text-fb-textDim hover:text-fb-text border border-fb-border/40 hover:border-fb-border/70 rounded-lg py-1.5 transition-colors';
|
||||||
|
state.verifyBtn.textContent = 'Verify tuning';
|
||||||
|
state.verifyBtn.title = 'Play each string in tune to confirm — marks your tuning verified';
|
||||||
|
state.verifyBtn.onclick = () => _startVerify();
|
||||||
|
state.verifyRow.appendChild(state.verifyBtn);
|
||||||
|
state.verifyStatus = document.createElement('div');
|
||||||
|
state.verifyStatus.className = 'text-[10px] text-fb-textDim text-center mt-1 hidden';
|
||||||
|
state.verifyRow.appendChild(state.verifyStatus);
|
||||||
|
state.uiContainer.appendChild(state.verifyRow);
|
||||||
|
|
||||||
// Auto-open nudge's explicit dismiss (hidden unless auto-opened; enable()
|
// Auto-open nudge's explicit dismiss (hidden unless auto-opened; enable()
|
||||||
// toggles it). Closes the same way as the × — disable().
|
// toggles it). Closes the same way as the × — disable().
|
||||||
const skipBtn = document.createElement('button');
|
const skipBtn = document.createElement('button');
|
||||||
@@ -778,6 +842,7 @@ window._tunerUI = function(state, actions) {
|
|||||||
renderTuningOptions,
|
renderTuningOptions,
|
||||||
renderStringNotes,
|
renderStringNotes,
|
||||||
updateUI,
|
updateUI,
|
||||||
|
resetVerify: resetVerifyUI,
|
||||||
updateInstrumentDisplay: _updateInstrumentDisplay,
|
updateInstrumentDisplay: _updateInstrumentDisplay,
|
||||||
updateSaveAsCustomVisibility: _updateSaveAsCustomVisibility,
|
updateSaveAsCustomVisibility: _updateSaveAsCustomVisibility,
|
||||||
updateFreeTuneUI,
|
updateFreeTuneUI,
|
||||||
|
|||||||
@@ -697,3 +697,56 @@ test('a transient /api/settings failure is not cached — the next coverage read
|
|||||||
const r2 = await api.coverageReport(DROP_D); // retry: settings now readable → a real report
|
const r2 = await api.coverageReport(DROP_D); // retry: settings now readable → a real report
|
||||||
assert.equal(r2.retune.length, 1, 'the retry actually computes coverage (Drop-D low string vs standard)');
|
assert.equal(r2.retune.length, 1, 'the retry actually computes coverage (Drop-D low string vs standard)');
|
||||||
});
|
});
|
||||||
|
// ── PR 9b: mic-verify (assumed -> verified via a per-string check) ──────────
|
||||||
|
const VERIFY_TARGETS = [82.41, 110, 146.83, 196, 246.94, 329.63]; // guitar-6 standard freqs
|
||||||
|
|
||||||
|
test('mic-verify: all strings in-tune-and-stable promotes to verified (with the confirmed offsets)', () => {
|
||||||
|
const sandbox = createTunerSandbox();
|
||||||
|
const sets = [];
|
||||||
|
sandbox.window.feedBack.workingTuning = { get: () => ({ offsets: [0, 0, 0, 0, 0, 0] }), set: (n, o) => sets.push({ n, o }) };
|
||||||
|
const api = sandbox.window._tunerAutoOpen;
|
||||||
|
assert.ok(api.verifyStart(VERIFY_TARGETS, [-2, 0, 0, 0, 0, 0])); // verifying a Drop-D tuning
|
||||||
|
for (let i = 0; i < 8; i++) api.verifyFeed(82.41, 2); // one string alone isn't enough
|
||||||
|
assert.equal(api.verifyState().complete, false);
|
||||||
|
for (const f of VERIFY_TARGETS) { for (let i = 0; i < 8; i++) api.verifyFeed(f, 2); }
|
||||||
|
assert.equal(api.verifyState().complete, true);
|
||||||
|
assert.equal(sets.length, 1);
|
||||||
|
assert.equal(sets[0].o.provenance, 'verified');
|
||||||
|
// The stamp carries the CONFIRMED offsets, not whatever stale offsets the slot held.
|
||||||
|
assert.deepEqual(sets[0].n.offsets, [-2, 0, 0, 0, 0, 0]);
|
||||||
|
assert.equal(sets[0].n.verifiedStrings.length, 6);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('mic-verify: an out-of-tune string never completes; cancel clears', () => {
|
||||||
|
const sandbox = createTunerSandbox();
|
||||||
|
sandbox.window.feedBack.workingTuning = { get: () => ({}), set() {} };
|
||||||
|
const api = sandbox.window._tunerAutoOpen;
|
||||||
|
api.verifyStart([82.41, 110]);
|
||||||
|
for (let i = 0; i < 30; i++) api.verifyFeed(82.41, 25); // 25c off -> never passes
|
||||||
|
assert.equal(api.verifyState().complete, false);
|
||||||
|
assert.deepEqual(api.verifyState().done, [false, false]);
|
||||||
|
api.verifyCancel();
|
||||||
|
assert.equal(api.verifyState(), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('mic-verify: the in-tune streak resets if a frame drifts out', () => {
|
||||||
|
const sandbox = createTunerSandbox();
|
||||||
|
sandbox.window.feedBack.workingTuning = { get: () => ({}), set() {} };
|
||||||
|
const api = sandbox.window._tunerAutoOpen;
|
||||||
|
api.verifyStart([100]);
|
||||||
|
for (let i = 0; i < 5; i++) api.verifyFeed(100, 2); // 5 in tune (need 8)
|
||||||
|
api.verifyFeed(100, 30); // drift out -> streak resets
|
||||||
|
for (let i = 0; i < 7; i++) api.verifyFeed(100, 2); // 7 more (not yet 8 in a row)
|
||||||
|
assert.equal(api.verifyState().done[0], false);
|
||||||
|
api.verifyFeed(100, 2); // 8th consecutive -> done
|
||||||
|
assert.equal(api.verifyState().complete, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the tuner exposes the mic-verify API and only it claims verified (screen.js)', () => {
|
||||||
|
const src = fs.readFileSync(TUNER_SCREEN_JS, 'utf8');
|
||||||
|
assert.match(src, /verifyStart:/);
|
||||||
|
assert.match(src, /verifyFeed:/);
|
||||||
|
assert.match(src, /provenance: 'verified'/); // the only place that stamps verified
|
||||||
|
// A just-earned 'verified' is not clobbered by the assumed publish-on-clear.
|
||||||
|
assert.match(src, /wasAutoOpened && !_verifiedPublished/);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user