Add new chart-transform plugin capability (#1000)

* Chart-transform plugin capability

* PR comments

* Cleanup

* Fix markdown

* CodeRabbit feedback

Signed-off-by: Joe <jphinspace@gmail.com>

---------

Signed-off-by: Joe <jphinspace@gmail.com>
Co-authored-by: Byron Gamatos <xasiklas@gmail.com>
This commit is contained in:
Joe Hallahan
2026-07-19 11:27:52 +02:00
committed by GitHub
co-authored by Byron Gamatos
parent f7942f3689
commit 05be9ebdbe
18 changed files with 1345 additions and 54 deletions
+335
View File
@@ -0,0 +1,335 @@
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 { createWindow, ROOT } = require('./capabilities_test_harness');
const CAPABILITIES_JS = path.join(ROOT, 'static', 'capabilities.js');
const CHART_TRANSFORM_JS = path.join(ROOT, 'static', 'capabilities', 'chart-transform.js');
const STORAGE_KEY = 'feedBack.chartTransform.selectedProviderId';
const PLUGIN_ID = 'example_plugin';
const PROVIDER_ID = 'example-transform';
const PROVIDER_LABEL = 'Example Transform';
function makeFakeHighway() {
const calls = { set: [], refresh: 0 };
return {
calls,
setChartTransform(p) { calls.set.push(p); },
refreshChartTransform() { calls.refresh += 1; },
getChartTransform() { return calls.set.length ? calls.set[calls.set.length - 1] : null; },
};
}
function loadChartTransform(options = {}) {
const window = createWindow(options);
// The real bus provides feedBack.on; the harness only has emit →
// dispatchEvent. Shim `on` the same way app.js implements it so the
// module's bus mirroring (song:ready, chart-transform-failed) is live.
window.feedBack.on = (type, handler) => window.addEventListener(type, handler);
if (options.highway) window.highway = options.highway;
if (options.persistedSelection) window.localStorage.setItem(STORAGE_KEY, options.persistedSelection);
const context = vm.createContext(window);
vm.runInContext(fs.readFileSync(CAPABILITIES_JS, 'utf8'), context, { filename: CAPABILITIES_JS });
vm.runInContext(fs.readFileSync(CHART_TRANSFORM_JS, 'utf8'), context, { filename: CHART_TRANSFORM_JS });
return window;
}
function captureEvents(api, eventNames) {
const events = [];
for (const name of eventNames) {
api.subscribe(name, (detail) => events.push(detail));
}
return events;
}
async function registerProvider(api, overrides = {}) {
return api.dispatch({
capability: 'chart-transform', command: 'register-provider',
source: overrides.source || PLUGIN_ID,
payload: {
providerId: overrides.providerId || PROVIDER_ID,
label: overrides.label || PROVIDER_LABEL,
transform: overrides.transform || ((input) => ({ notes: input.notes })),
},
});
}
test('chart-transform domain registers a safe provider-coordinator owner', () => {
const window = loadChartTransform();
const api = window.feedBack.capabilities;
const pipeline = api.inspect('chart-transform');
assert.ok(pipeline, 'chart-transform pipeline exists');
const owner = (pipeline.participants || []).find(p => p.pluginId === 'core.chart-transform');
assert.ok(owner, 'core.chart-transform owner registered');
assert.equal(owner.safety, 'safe');
assert.ok(owner.commands.includes('select-provider'));
assert.ok(owner.commands.includes('refresh'));
assert.equal(window.feedBack.chartTransformDomain.version, 1);
});
test('register-provider requires a transform function', async () => {
const window = loadChartTransform();
const api = window.feedBack.capabilities;
const result = await api.dispatch({
capability: 'chart-transform', command: 'register-provider',
source: PLUGIN_ID, payload: { providerId: PROVIDER_ID },
});
assert.equal(result.outcome, 'degraded');
assert.match(result.reason, /transform\(input\) function/);
});
test('register + select installs the provider on the highway and persists', async () => {
const highway = makeFakeHighway();
const window = loadChartTransform({ highway });
const api = window.feedBack.capabilities;
const events = captureEvents(api, [
'chart-transform:provider-registered',
'chart-transform:transform-changed',
]);
const reg = await registerProvider(api);
assert.equal(reg.outcome, 'handled');
assert.ok(api.inspect('chart-transform').participants.some(p => p.pluginId === PLUGIN_ID));
const sel = await api.dispatch({
capability: 'chart-transform', command: 'select-provider',
source: 'settings_ui', payload: { providerId: PROVIDER_ID },
});
assert.equal(sel.outcome, 'handled');
assert.equal(sel.payload.active, PROVIDER_ID);
assert.equal(sel.payload.installed, true);
assert.equal(highway.calls.set.length, 1);
assert.equal(highway.calls.set[0].id, PROVIDER_ID);
assert.equal(typeof highway.calls.set[0].transform, 'function');
assert.equal(window.localStorage.getItem(STORAGE_KEY), PROVIDER_ID);
const names = events.map(e => e.event);
assert.ok(names.includes('provider-registered'));
assert.ok(names.includes('transform-changed'));
});
test('select-provider with an unknown id degrades', async () => {
const window = loadChartTransform();
const api = window.feedBack.capabilities;
const result = await api.dispatch({
capability: 'chart-transform', command: 'select-provider',
source: 'settings_ui', payload: { providerId: 'nope' },
});
assert.equal(result.outcome, 'degraded');
assert.match(result.reason, /Unknown chart-transform provider/);
});
test('selection without a highway is kept and installed on song:ready', async () => {
const window = loadChartTransform();
const api = window.feedBack.capabilities;
await registerProvider(api);
const sel = await api.dispatch({
capability: 'chart-transform', command: 'select-provider',
source: 'settings_ui', payload: { providerId: PROVIDER_ID },
});
assert.equal(sel.outcome, 'handled');
assert.equal(sel.payload.installed, false, 'no highway yet');
const highway = makeFakeHighway();
window.highway = highway;
window.feedBack.emit('song:ready', {});
assert.equal(highway.calls.set.length, 1);
assert.equal(highway.calls.set[0].id, PROVIDER_ID);
assert.equal(window.feedBack.chartTransformDomain.snapshot().installed, true);
});
test('a persisted selection restores when its provider registers', async () => {
const highway = makeFakeHighway();
const window = loadChartTransform({ highway, persistedSelection: PROVIDER_ID });
const api = window.feedBack.capabilities;
await registerProvider(api);
const snapshot = window.feedBack.chartTransformDomain.snapshot();
assert.equal(snapshot.active, PROVIDER_ID);
assert.equal(snapshot.activeSource, 'restore-selection');
assert.equal(highway.calls.set.length, 1);
});
test('unregister is registrant-only and detaches the active provider', async () => {
const highway = makeFakeHighway();
const window = loadChartTransform({ highway });
const api = window.feedBack.capabilities;
await registerProvider(api);
await api.dispatch({
capability: 'chart-transform', command: 'select-provider',
source: 'settings_ui', payload: { providerId: PROVIDER_ID },
});
const denied = await api.dispatch({
capability: 'chart-transform', command: 'unregister-provider',
source: 'someone_else', payload: { providerId: PROVIDER_ID },
});
assert.equal(denied.outcome, 'degraded');
assert.match(denied.reason, /original registrant/);
const ok = await api.dispatch({
capability: 'chart-transform', command: 'unregister-provider',
source: PLUGIN_ID, payload: { providerId: PROVIDER_ID },
});
assert.equal(ok.outcome, 'handled');
const snapshot = window.feedBack.chartTransformDomain.snapshot();
assert.equal(snapshot.active, null);
assert.equal(snapshot.providers.length, 0);
// Detach = a trailing setChartTransform(null) on the highway.
assert.equal(highway.calls.set[highway.calls.set.length - 1], null);
// Persisted selection survives so re-registration re-activates.
assert.equal(window.localStorage.getItem(STORAGE_KEY), PROVIDER_ID);
});
test('unregister keeps a participant while another provider still references it', async () => {
const window = loadChartTransform();
const api = window.feedBack.capabilities;
await registerProvider(api, { providerId: 'provider-a', label: 'Provider A' });
await registerProvider(api, { providerId: 'provider-b', label: 'Provider B' });
let participant = api.inspect('chart-transform').participants
.find(p => p.pluginId === PLUGIN_ID);
assert.deepEqual(Array.from(participant.providerPolicy.providerIds), ['provider-a', 'provider-b']);
assert.deepEqual(
Array.from(participant.providerPolicy.providers, p => ({ id: p.id, label: p.label })),
[{ id: 'provider-a', label: 'Provider A' }, { id: 'provider-b', label: 'Provider B' }],
);
await api.dispatch({
capability: 'chart-transform', command: 'unregister-provider',
source: PLUGIN_ID, payload: { providerId: 'provider-b' },
});
participant = api.inspect('chart-transform').participants
.find(p => p.pluginId === PLUGIN_ID);
assert.ok(participant, 'the shared participant remains registered');
assert.deepEqual(Array.from(participant.providerPolicy.providerIds), ['provider-a']);
assert.deepEqual(
Array.from(participant.providerPolicy.providers, p => ({ id: p.id, label: p.label })),
[{ id: 'provider-a', label: 'Provider A' }],
);
assert.deepEqual(
Array.from(window.feedBack.chartTransformDomain.snapshot().providers, p => p.id),
['provider-a'],
);
await api.dispatch({
capability: 'chart-transform', command: 'unregister-provider',
source: PLUGIN_ID, payload: { providerId: 'provider-a' },
});
assert.ok(!api.inspect('chart-transform').participants
.some(p => p.pluginId === PLUGIN_ID), 'the final removal unregisters the participant');
});
test('clear-provider clears the highway hook and the persisted selection', async () => {
const highway = makeFakeHighway();
const window = loadChartTransform({ highway });
const api = window.feedBack.capabilities;
await registerProvider(api);
await api.dispatch({
capability: 'chart-transform', command: 'select-provider',
source: 'settings_ui', payload: { providerId: PROVIDER_ID },
});
const result = await api.dispatch({
capability: 'chart-transform', command: 'clear-provider', source: 'settings_ui',
});
assert.equal(result.outcome, 'handled');
assert.equal(highway.calls.set[highway.calls.set.length - 1], null);
assert.equal(window.localStorage.getItem(STORAGE_KEY), null);
});
test('refresh re-runs the installed transform', async () => {
const highway = makeFakeHighway();
const window = loadChartTransform({ highway });
const api = window.feedBack.capabilities;
await registerProvider(api);
await api.dispatch({
capability: 'chart-transform', command: 'select-provider',
source: 'settings_ui', payload: { providerId: PROVIDER_ID },
});
const result = await api.dispatch({ capability: 'chart-transform', command: 'refresh', source: PLUGIN_ID });
assert.equal(result.outcome, 'handled');
assert.equal(result.payload.refreshed, true);
assert.equal(highway.calls.refresh, 1);
});
test('announced highway instances (splitscreen panels) get the active transform', async () => {
const primary = makeFakeHighway();
const window = loadChartTransform({ highway: primary });
const api = window.feedBack.capabilities;
await registerProvider(api);
await api.dispatch({
capability: 'chart-transform', command: 'select-provider',
source: 'settings_ui', payload: { providerId: PROVIDER_ID },
});
assert.equal(window.feedBack.chartTransformDomain.snapshot().surfaces, 1);
// A splitscreen panel announces its own createHighway() instance.
const panel = makeFakeHighway();
window.feedBack.emit('highway:created', { highway: panel });
assert.equal(panel.calls.set.length, 1, 'panel receives the active transform');
assert.equal(panel.calls.set[0].id, PROVIDER_ID);
assert.equal(window.feedBack.chartTransformDomain.snapshot().surfaces, 2);
// Refresh reaches every surface.
await api.dispatch({ capability: 'chart-transform', command: 'refresh', source: PLUGIN_ID });
assert.equal(primary.calls.refresh, 1);
assert.equal(panel.calls.refresh, 1);
// Clearing detaches every surface.
await api.dispatch({ capability: 'chart-transform', command: 'clear-provider', source: 'settings_ui' });
assert.equal(primary.calls.set[primary.calls.set.length - 1], null);
assert.equal(panel.calls.set[panel.calls.set.length - 1], null);
});
test('a panel announced before any selection installs on later select', async () => {
const window = loadChartTransform();
const api = window.feedBack.capabilities;
const panel = makeFakeHighway();
window.feedBack.emit('highway:created', { highway: panel });
await registerProvider(api);
const sel = await api.dispatch({
capability: 'chart-transform', command: 'select-provider',
source: 'settings_ui', payload: { providerId: PROVIDER_ID },
});
assert.equal(sel.outcome, 'handled');
assert.equal(panel.calls.set.length, 1);
assert.equal(panel.calls.set[0].id, PROVIDER_ID);
});
test('highway failure events expose a fixed public reason', async () => {
const highway = makeFakeHighway();
const window = loadChartTransform({ highway });
const api = window.feedBack.capabilities;
const events = captureEvents(api, ['chart-transform:transform-failed']);
await registerProvider(api);
await api.dispatch({
capability: 'chart-transform', command: 'select-provider',
source: 'settings_ui', payload: { providerId: PROVIDER_ID },
});
window.feedBack.emit('highway:chart-transform-failed', {
id: PROVIDER_ID,
reason: 'token=secret https://example.test/private chart={notes:[...]}',
});
const snapshot = window.feedBack.chartTransformDomain.snapshot();
assert.equal(snapshot.lastFailure.providerId, PROVIDER_ID);
assert.equal(snapshot.lastFailure.reason, 'Chart transform provider failed');
assert.equal(events.length, 1);
assert.equal(events[0].payload.reason, 'Chart transform provider failed');
});
test('diagnostics contribution carries the schema and no song identity fields', async () => {
const window = loadChartTransform();
const api = window.feedBack.capabilities;
await registerProvider(api);
const contributions = window.feedBack.diagnostics.snapshotContributions();
const diag = contributions['chart-transform-capability'];
assert.ok(diag, 'diagnostics contributed');
assert.equal(diag.schema, 'feedBack.chart_transform.diagnostics.v1');
const flat = JSON.stringify(diag);
assert.ok(!/filename|title|artist|arrangement/.test(flat), 'no song identity in diagnostics');
});
+355
View File
@@ -0,0 +1,355 @@
// Source-level coverage is used because createHighway's browser closure is too
// large for the Node harness. Critical staging helpers are exercised directly.
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 highwayJs = path.join(__dirname, '..', '..', 'static', 'highway.js');
const highwayDrawJs = path.join(__dirname, '..', '..', 'static', 'js', 'highway-draw.js');
function extractBlock(src, marker) {
const start = src.indexOf(marker);
assert.ok(start >= 0, `${marker} present`);
const open = src.indexOf('{', start);
assert.ok(open >= 0, `${marker} has a body`);
let depth = 0;
for (let i = open; i < src.length; i++) {
if (src[i] === '{') depth += 1;
else if (src[i] === '}') {
depth -= 1;
if (depth === 0) return src.slice(start, i + 1);
}
}
assert.fail(`${marker} body is balanced`);
}
test('highway public API exposes the chart-transform hook', () => {
const src = fs.readFileSync(highwayJs, 'utf8');
assert.match(src, /setChartTransform\s*\(\s*p\s*\)\s*\{/, 'setChartTransform exists');
assert.match(src, /getChartTransform\s*\(\s*\)\s*\{[^}]*_xfProvider/, 'getChartTransform returns the provider');
assert.match(src, /refreshChartTransform\s*\(\s*\)\s*\{[^}]*_restageChartTransform/, 'refreshChartTransform restages');
});
test('restage runs at BOTH exits of _rebuildMasteryFilter (transform after difficulty)', () => {
const src = fs.readFileSync(highwayJs, 'utf8');
const fnStart = src.indexOf('function _rebuildMasteryFilter()');
const fnEnd = src.indexOf('function _clearChartTransformStage');
assert.ok(fnStart > -1 && fnEnd > fnStart, 'both functions present in order');
const body = src.slice(fnStart, fnEnd);
const calls = body.match(/_restageChartTransform\(\);/g) || [];
assert.equal(calls.length, 2, 'restage at the early return and the normal exit');
});
test('restage consumes the difficulty-filtered arrays, not the raw chart', () => {
const src = fs.readFileSync(highwayJs, 'utf8');
const fn = src.slice(src.indexOf('function _restageChartTransform'), src.indexOf('// ── Public API'));
assert.match(fn, /notes:\s*filterActive\s*\?\s*hwState\._filteredNotes\s*:\s*hwState\.notes/);
assert.match(fn, /allNotes:\s*hwState\.notes/, 'full-difficulty views passed alongside');
});
test('a throwing provider clears the stage and emits highway:chart-transform-failed', () => {
const src = fs.readFileSync(highwayJs, 'utf8');
const fn = src.slice(src.indexOf('function _restageChartTransform'), src.indexOf('// ── Public API'));
const report = extractBlock(src, 'function _reportChartTransformFailure(provider, error)');
assert.match(fn, /catch\s*\(e\)\s*\{[\s\S]*_reportChartTransformFailure\(p, e\)[\s\S]*return;/);
assert.match(fn, /^\s*_clearChartTransformStage\(\);/m, 'stage cleared before the provider runs');
// Execute the extracted reporter with a sentinel error: the emitted
// payload must contain ONLY the approved field (id) — nothing derived
// from the exception — while the raw error stays on the local console.
const sandbox = { cleared: 0, emitted: [], logged: [] };
vm.runInNewContext(`
const _clearChartTransformStage = () => { cleared += 1; };
const console = { error: (...args) => logged.push(args) };
const window = { feedBack: { emit: (type, detail) => emitted.push({ type, detail }) } };
${report}
_reportChartTransformFailure({ id: 'prov-1' }, new Error('sentinel: /Users/someone/secret.sloppak'));
`, sandbox);
assert.equal(sandbox.cleared, 1, 'failure clears the stage');
assert.equal(sandbox.emitted.length, 1, 'exactly one failure event');
assert.equal(sandbox.emitted[0].type, 'highway:chart-transform-failed');
assert.deepEqual(Object.keys(sandbox.emitted[0].detail), ['id'],
'payload carries only the approved field — no exception-derived fields');
assert.equal(sandbox.emitted[0].detail.id, 'prov-1');
assert.ok(!JSON.stringify(sandbox.emitted[0].detail).includes('sentinel'),
'nothing exception-derived leaks into the event');
assert.equal(sandbox.logged.length, 1, 'raw exception stays on the local console');
assert.ok(sandbox.logged[0].some((arg) => String(arg).includes('sentinel')),
'the local console received the actual error');
});
test('restage is a pre-ready no-op: provider stays attached, ready path runs the first staging', () => {
const src = fs.readFileSync(highwayJs, 'utf8');
const fn = src.slice(src.indexOf('function _restageChartTransform'), src.indexOf('// ── Public API'));
const guardAt = fn.indexOf('if (!hwState.ready) return;');
const invokeAt = fn.indexOf('p.transform(');
assert.ok(guardAt > -1, 'ready guard present');
assert.ok(invokeAt > guardAt, 'guard sits before the provider is invoked');
// The ready handler must flip hwState.ready BEFORE rebuilding the
// filter, or the guard would skip the first real staging.
const readyCase = src.indexOf("case 'ready':");
const readyFlip = src.indexOf('hwState.ready = true;', readyCase);
const readyRebuild = src.indexOf('_rebuildMasteryFilter();', readyCase);
assert.ok(readyCase > -1 && readyFlip > -1 && readyRebuild > readyFlip,
'ready handler sets hwState.ready before the rebuild that restages');
});
test('bundle assembly prefers the staged transform views', () => {
const src = fs.readFileSync(highwayJs, 'utf8');
assert.match(src, /b\.notes = hwState\._xfNotes !== null \? hwState\._xfNotes/);
assert.match(src, /b\.chords = hwState\._xfChords !== null \? hwState\._xfChords/);
assert.match(src, /b\.anchors = hwState\._xfAnchors !== null \? hwState\._xfAnchors/);
assert.match(src, /b\.chordTemplates = hwState\._xfChordTemplates !== null/);
assert.match(src, /b\.stringCount = hwState\._xfStringCount !== null/);
assert.match(src, /b\.tuning = hwState\._xfTuning !== null/);
assert.match(src, /b\.capo = hwState\._xfCapo !== null/);
assert.match(src, /b\.handShapes = hwState\._xfHandShapes !== null \? hwState\._xfHandShapes/);
assert.match(src, /b\.centOffset = hwState\._xfCentOffset !== null/);
});
test('transform input carries the effective handShapes; output stages handShapes/centOffset', () => {
const src = fs.readFileSync(highwayJs, 'utf8');
const fn = src.slice(src.indexOf('function _restageChartTransform'), src.indexOf('// ── Public API'));
assert.match(fn, /handShapes: \(hwState\._filteredHandShapes !== null && hwState\._phrasesHaveHandShapes\)/,
'input handShapes uses the same effective selection as the bundle');
assert.match(fn, /_sortedChartTransformArray\(out\.handShapes, 'start_time'\)/);
assert.match(fn, /if \(Number\.isFinite\(out\.centOffset\)\) hwState\._xfCentOffset = out\.centOffset;/);
});
test('unordered provider timelines are copied and normalized for searches and anchor scans', () => {
const highwaySrc = fs.readFileSync(highwayJs, 'utf8');
const drawSrc = fs.readFileSync(highwayDrawJs, 'utf8');
const snippets = [
extractBlock(highwaySrc, 'function _clearChartTransformStage()'),
extractBlock(highwaySrc, 'function _cloneChartTransformValue(value, seen = new WeakMap())'),
extractBlock(highwaySrc, 'function _sortedChartTransformArray(items, key)'),
extractBlock(highwaySrc, 'function _reportChartTransformFailure(provider, error)'),
extractBlock(highwaySrc, 'function _restageChartTransform()'),
extractBlock(highwaySrc, 'function bsearchTime(arr, time)'),
extractBlock(highwaySrc, 'function getAnchorAt(t)'),
extractBlock(highwaySrc, 'function getMaxFretInWindow(t)'),
extractBlock(drawSrc, 'export function bsearch(arr, time)').replace('export ', ''),
].join('\n');
const providerOutput = {
notes: [{ t: 9 }, { t: 1 }, { t: 5 }],
chords: [{ t: 8 }, { t: 2 }],
anchors: [
{ time: 10, fret: 20, width: 2 },
{ time: 0, fret: 1, width: 3 },
{ time: 5, fret: 10, width: 4 },
],
allNotes: [{ t: 7 }, { t: 0 }, { t: 3 }],
allChords: [{ t: 6 }, { t: 4 }],
handShapes: [{ start_time: 9 }, { start_time: 1 }],
stringCount: 4,
tuning: [-2, -2, -2, -2],
capo: 2,
centOffset: -12.5,
};
const hwState = {
ready: true,
_xfProvider: { id: 'unordered', transform: () => providerOutput },
_filteredNotes: [],
_filteredChords: [],
_filteredAnchors: [],
_filteredHandShapes: [],
_phrasesHaveHandShapes: true,
notes: [], chords: [], anchors: [], handShapes: [], chordTemplates: [],
stringCount: 6, songInfo: {},
};
const helpers = new Function('hwState', 'window', 'VISIBLE_SECONDS', 'console', `
${snippets}
return { _restageChartTransform, bsearch, bsearchTime, getAnchorAt, getMaxFretInWindow };
`)(hwState, {}, 3, { error() {} });
helpers._restageChartTransform();
assert.deepEqual(hwState._xfNotes.map(n => n.t), [1, 5, 9]);
assert.deepEqual(hwState._xfChords.map(ch => ch.t), [2, 8]);
assert.deepEqual(hwState._xfNotesAll.map(n => n.t), [0, 3, 7]);
assert.deepEqual(hwState._xfChordsAll.map(ch => ch.t), [4, 6]);
assert.deepEqual(hwState._xfAnchors.map(a => a.time), [0, 5, 10]);
assert.deepEqual(hwState._xfHandShapes.map(h => h.start_time), [1, 9]);
assert.equal(hwState._xfStringCount, 4);
assert.deepEqual(hwState._xfTuning, [-2, -2, -2, -2]);
assert.equal(hwState._xfCapo, 2);
assert.equal(hwState._xfCentOffset, -12.5);
assert.deepEqual(providerOutput.notes.map(n => n.t), [9, 1, 5], 'provider output is not mutated');
providerOutput.tuning[0] = 99;
assert.equal(hwState._xfTuning[0], -2, 'staged metadata is detached from provider output');
assert.equal(helpers.bsearch(hwState._xfNotes, 5), 1);
assert.equal(helpers.bsearchTime(hwState._xfAnchors, 5), 1);
assert.equal(helpers.getAnchorAt(6).time, 5);
assert.equal(helpers.getMaxFretInWindow(0), 14);
hwState._filteredNotes = null;
hwState._filteredChords = null;
hwState._xfProvider.transform = () => ({
notes: [{ t: 4 }, { t: 2 }],
chords: [{ t: 3 }, { t: 1 }],
});
helpers._restageChartTransform();
assert.deepEqual(hwState._xfNotesAll.map(n => n.t), [2, 4], 'unfiltered notes still fall back');
assert.deepEqual(hwState._xfChordsAll.map(ch => ch.t), [1, 3], 'unfiltered chords still fall back');
});
test('provider inputs and staged outputs are isolated from provider mutation', () => {
const src = fs.readFileSync(highwayJs, 'utf8');
const snippets = [
extractBlock(src, 'function _clearChartTransformStage()'),
extractBlock(src, 'function _cloneChartTransformValue(value, seen = new WeakMap())'),
extractBlock(src, 'function _sortedChartTransformArray(items, key)'),
extractBlock(src, 'function _reportChartTransformFailure(provider, error)'),
extractBlock(src, 'function _restageChartTransform()'),
].join('\n');
const sourceNote = { t: 1, bendValues: [{ t: 0, v: 1 }] };
const sourceInfo = { tuning: [0, 0], nested: { value: 1 } };
const events = [];
const hwState = {
ready: true,
_xfProvider: null,
_filteredNotes: null, _filteredChords: null, _filteredAnchors: null,
_filteredHandShapes: null, _phrasesHaveHandShapes: false,
notes: [sourceNote], chords: [], anchors: [], handShapes: [], chordTemplates: [],
stringCount: 2, songInfo: sourceInfo,
};
const helpers = new Function('hwState', 'window', 'console', `
${snippets}
return { _restageChartTransform };
`)(hwState, { feedBack: { emit(name, detail) { events.push({ name, detail }); } } }, { error() {} });
hwState._xfProvider = {
id: 'mutating-provider',
transform(input) {
input.notes[0].t = 99;
input.notes[0].bendValues[0].v = 7;
input.songInfo.nested.value = 8;
throw new Error('private provider detail');
},
};
helpers._restageChartTransform();
assert.equal(sourceNote.t, 1);
assert.equal(sourceNote.bendValues[0].v, 1);
assert.equal(sourceInfo.nested.value, 1);
assert.equal(hwState._xfNotes, null);
assert.deepEqual(events.map(event => event.name), ['highway:chart-transform-failed']);
const output = { notes: [{ t: 2, nested: { value: 3 } }] };
hwState._xfProvider = { id: 'stable-provider', transform: () => output };
helpers._restageChartTransform();
output.notes[0].t = 20;
output.notes[0].nested.value = 30;
assert.equal(hwState._xfNotes[0].t, 2);
assert.equal(hwState._xfNotes[0].nested.value, 3);
});
test('async and malformed provider outputs fail closed without a partial stage', async () => {
const src = fs.readFileSync(highwayJs, 'utf8');
const snippets = [
extractBlock(src, 'function _clearChartTransformStage()'),
extractBlock(src, 'function _cloneChartTransformValue(value, seen = new WeakMap())'),
extractBlock(src, 'function _sortedChartTransformArray(items, key)'),
extractBlock(src, 'function _reportChartTransformFailure(provider, error)'),
extractBlock(src, 'function _restageChartTransform()'),
].join('\n');
const events = [];
const errors = [];
const hwState = {
ready: true,
_xfProvider: { id: 'async-provider', transform: async () => { throw new Error('async detail'); } },
_filteredNotes: null, _filteredChords: null, _filteredAnchors: null,
_filteredHandShapes: null, _phrasesHaveHandShapes: false,
notes: [], chords: [], anchors: [], handShapes: [], chordTemplates: [],
stringCount: 6, songInfo: {},
};
const helpers = new Function('hwState', 'window', 'console', `
${snippets}
return { _restageChartTransform };
`)(hwState, { feedBack: { emit(name) { events.push(name); } } }, { error(...args) { errors.push(args); } });
helpers._restageChartTransform();
assert.equal(hwState._xfNotes, null);
assert.equal(events.length, 1);
assert.match(String(errors[0][1]), /must return synchronously/);
await new Promise(resolve => setImmediate(resolve));
assert.match(String(errors[1][1]), /async detail/, 'async rejection stays in the local console');
const output = { chords: [{ t: 1 }] };
Object.defineProperty(output, 'notes', { enumerable: true, get() { throw new Error('bad getter'); } });
hwState._xfProvider = { id: 'getter-provider', transform: () => output };
helpers._restageChartTransform();
assert.equal(hwState._xfNotes, null);
assert.equal(hwState._xfChords, null);
assert.equal(events.length, 2);
});
test('createHighway announces each instance via highway:created', () => {
const src = fs.readFileSync(highwayJs, 'utf8');
assert.match(src, /emit\('highway:created', \{ highway: api \}\)/,
'factory emits highway:created with the api instance');
});
test('public getters fall through transformed → filtered → raw', () => {
const src = fs.readFileSync(highwayJs, 'utf8');
assert.match(src, /getNotes\(\)\s*\{\s*return hwState\._xfNotesAll !== null/);
assert.match(src, /getChords\(\)\s*\{\s*return hwState\._xfChordsAll !== null/);
assert.match(src, /getFilteredNotes\(\)\s*\{\s*if \(hwState\._xfNotes !== null\) return hwState\._xfNotes;/);
assert.match(src, /getFilteredChords\(\)\s*\{\s*if \(hwState\._xfChords !== null\) return hwState\._xfChords;/);
assert.match(src, /getChordTemplates\(\)\s*\{\s*return hwState\._xfChordTemplates !== null/);
assert.match(src, /getStringCount\(\)\s*\{\s*return hwState\._xfStringCount !== null/);
assert.match(src, /getTuning\(\)\s*\{\s*return hwState\._xfTuning !== null/);
assert.match(src, /getCapo\(\)\s*\{\s*return hwState\._xfCapo !== null/);
assert.match(src, /getCentOffset\(\)\s*\{\s*return hwState\._xfCentOffset !== null/);
assert.match(src, /getSongInfo\(\)\s*\{\s*return hwState\.songInfo;\s*\}/,
'getSongInfo keeps the original chart metadata contract');
});
test('anchor zoom helpers read the staged anchors first', () => {
const src = fs.readFileSync(highwayJs, 'utf8');
const anchorSites = src.match(/hwState\._xfAnchors !== null \? hwState\._xfAnchors\s*\n?\s*: hwState\._filteredAnchors !== null/g) || [];
assert.ok(anchorSites.length >= 2, 'getAnchorAt and getMaxFretInWindow both staged-aware');
});
test('init and reconnect clear the stage but keep the provider', () => {
const src = fs.readFileSync(highwayJs, 'utf8');
const initBody = extractBlock(src, 'init(canvasEl, container)');
const reconnectBody = extractBlock(src, 'reconnect(filename, arrangement)');
assert.match(initBody, /_clearChartTransformStage\(\);/, 'init clears the stage');
assert.match(reconnectBody, /_clearChartTransformStage\(\);/, 'reconnect clears the stage');
assert.ok(!/init\([\s\S]{0,2000}_xfProvider = null/.test(src.slice(src.indexOf('const api = {'))),
'api reset paths never drop the installed provider');
});
test('default 2D draw path prefers the staged views (drawNotes/drawChords/drawSustains)', () => {
const src = fs.readFileSync(highwayDrawJs, 'utf8');
const noteSites = src.match(/hwState\._xfNotes !== null \? hwState\._xfNotes/g) || [];
assert.ok(noteSites.length >= 2, 'drawNotes and drawSustains staged-aware');
assert.match(src, /hwState\._xfChords !== null \? hwState\._xfChords/, 'drawChords staged-aware');
});
test('highway_3d nut labels prefer the transform-aware bundle tuning/capo', () => {
const src = fs.readFileSync(path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'screen.js'), 'utf8');
assert.match(src, /let tuning = Array\.isArray\(bundle\.tuning\) \? bundle\.tuning : \(songInfo && songInfo\.tuning\)/,
'label derivation reads a well-formed bundle.tuning first, songInfo otherwise');
assert.match(src, /let cap = bundle\.capo;/,
'label derivation reads bundle.capo first');
// Both cache paths must key on the same bundle-first capo the labels
// use (songInfo stays as the fallback branch of each ternary), and all
// three sites share the same final fallback (0) so cache signatures
// match rendered output.
assert.match(src, /const capo =\s*\n\s*bundle && Number\.isFinite\(bundle\.capo\) \? bundle\.capo\s*\n\s*: \(si && Number\.isFinite\(si\.capo\) \? si\.capo : 0\)/,
'label signature keys on bundle.capo first with a 0 fallback');
assert.match(src, /const capo =\s*\n\s*Number\.isFinite\(bundle\.capo\) \? bundle\.capo\s*\n\s*: \(si && Number\.isFinite\(si\.capo\) \? si\.capo : 0\)/,
'cheap-key fast path keys on bundle.capo first with a 0 fallback');
});
test('chord template reads route through the effective-templates helper', () => {
const src = fs.readFileSync(highwayDrawJs, 'utf8');
assert.match(src, /export function _effChordTemplates\(hwState\)/);
assert.ok(!/getChordTemplateInfo\([^)]*,\s*hwState\.chordTemplates\)/.test(src),
'no direct hwState.chordTemplates read remains at template-info call sites');
assert.match(src, /_chordRenderCacheTemplates !== effTemplates/, 'render cache keys on effective templates');
});
+4 -2
View File
@@ -51,8 +51,10 @@ test('_ensureChordRenderCache keys off src, _inverted, AND chordTemplates', () =
);
assert.match(src, eqEither('hwState\\._chordRenderCacheSrc', 'src'), 'cache must key on src');
assert.match(src, eqEither('hwState\\._chordRenderCacheInverted', 'hwState\\._inverted'), 'cache must key on _inverted');
assert.match(src, neqEither('hwState\\._chordRenderCacheTemplates', 'hwState\\.chordTemplates'),
'cache must key on chordTemplates (detected via !== for change-flag)');
assert.match(src, neqEither('hwState\\._chordRenderCacheTemplates', 'effTemplates'),
'cache must key on the effective chordTemplates (detected via !== for change-flag)');
assert.match(src, /_effChordTemplates\(hwState\)\s*\{\s*\n?\s*return hwState\._xfChordTemplates !== null \? hwState\._xfChordTemplates : hwState\.chordTemplates;/,
'effective templates must derive from hwState.chordTemplates');
});
test('chordTemplates change resets fretline preview and frame-mismatch warner', () => {