mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-12 11:49:28 +00:00
rename: slopsmith → feedBack, byron → got-feedBack (#537)
* Update GitHub repo references from feedback* to feedBack* * rename: slopsmith -> feedBack, byron -> got-feedBack Renames across the entire codebase: - slopsmith/Slopsmith/SLOPSMITH/SlopSmith -> feedBack/FeedBack/FEEDBACK/FeedBack - byron/Byron/Byrongamatos -> got-feedBack/got-feedBack/got-feedBack - /home/byron/ -> /opt/got-feedBack/ - byron@ougsoft.com -> hi@got-feedBack.org - github.com/byrongamatos/ -> github.com/got-feedback/ - com.byron. -> com.got-feedback. - SLOPSMITH_ env vars -> FEEDBACK_ with backward-compat fallback - Protocol/storage strings migrated with read-old/write-new pattern - window.slopsmith JS API -> window.feedBack (canonical) + backward-compat alias Refs: #rename-slopsmith * rename: complete regen against current main + fix backward-compat alias Regenerated the slopsmith->feedBack / byron->got-feedBack rename on top of current main (3 commits had landed since the branch: #572/#554/#574), resolving the four content conflicts in favour of main's newer content (autoplay/auto-exit, accuracy-badge, Virtuoso re-home, feedpak badge). Completion fixes on top of the mechanical rename: - Re-apply rename to post-branch content the original rename never saw: window.slopsmith(.Tour) consumers in lessons.js / notifications.js / onboarding-tour.js, and the matching JS + python tests (autoplay_exit, progression_*, test_feedpak_extension FEEDBACK_* env vars). The test env vars now match server.py (which reads FEEDBACK_SYNC_STARTUP / FEEDBACK_SKIP_STARTUP_TASKS), so the sync-startup test exercises the real path again. - Restore the window.slopsmith backward-compat alias dropped during conflict resolution, and move the bus aliases to AFTER the _feedBackExisting merge block so they reference the fully-assembled object (also fixes the loop_api.test.js API-surface regex, which the original PR latently broke). - Drop the stray empty data/web_library.db (runtime DB lives in CONFIG_DIR) and gitignore it. - Fix stale tone-source test: feed[dB]ack -> fee[dB]ack to match shipped source labels. Verified locally (org CI billing-blocked): JS 819/819 pass; pytest 1669 passed / 1683 collected with 0 import errors; zero residual slopsmith/byron except the two intentional window.slopsmith aliases. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * rename: implement advertised backward-compat + prune dead community plugins Address gaps where PR #537's "Backward compatibility" section was advertised but not implemented, and clean up the community plugin list. Env vars (FEEDBACK_* canonical, legacy SLOPSMITH_* honoured): - New lib/env_compat.py (getenv_compat / env_flag_compat) + tests. server.py (_env_flag + all FEEDBACK_* reads), diagnostics_hardware, gp2midi and tailwind_rebuild now resolve the legacy alias, so existing SLOPSMITH_UI / SLOPSMITH_PLUGINS_DIR / etc. deployments keep working. - Fix the rename collapsing plugins/__init__.py and minigames/routes.py from `FEEDBACK_PLUGINS_DIR or SLOPSMITH_PLUGINS_DIR` into a redundant `FEEDBACK_ or FEEDBACK_` (the fallback was silently lost). Storage (app.js update-channel): - Read feedBack-update-channel, fall back to legacy slopsmith-update-channel, and clear the legacy key on write — so a user's update-channel preference survives the rename instead of resetting to "stable". Community plugin list (README): the rename rewrote third-party repo URLs we don't own. Probed every one; their owners never renamed, so: - Restore the 13 live community plugins to their real slopsmith-* names. - Prune 6 that are 404 to the public (topkoa splitscreen/stems, OmikronApex tuner, Jafz2001 nam-rig-builder, DeathlySin song-preview, Erikcb91 shuffle). - Fix a pre-existing Guitar Theory clone-command typo (nam-tone -> guitar-theory). Verified: env_compat 7/7, JS 819/819, pytest 1690 collected / 0 import errors, rename-sensitive + startup suites green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: byrongamatos <xasiklas@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
byrongamatos
parent
a8ad02739a
commit
af2949677a
@@ -18,7 +18,7 @@ function registerProvider(api, overrides = {}) {
|
||||
'chain.resolve': () => ({
|
||||
outcome: 'handled',
|
||||
plan: {
|
||||
schema: 'slopsmith.audio_effects.chain_plan.v1',
|
||||
schema: 'feedBack.audio_effects.chain_plan.v1',
|
||||
planId: 'plan-1',
|
||||
routeKey: 'desktop-main',
|
||||
providerId: overrides.providerId || 'rig-builder',
|
||||
@@ -42,18 +42,18 @@ function registerProvider(api, overrides = {}) {
|
||||
|
||||
test('audio-effects host registers active domain and contributes diagnostics', () => {
|
||||
const window = loadAudioEffects();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
const pipeline = api.inspect('audio-effects');
|
||||
const diagnostics = window.slopsmith.diagnostics.snapshotContributions();
|
||||
const diagnostics = window.feedBack.diagnostics.snapshotContributions();
|
||||
|
||||
assert.equal(pipeline.review.lifecycle, 'active');
|
||||
assert.equal(pipeline.participants.some(p => p.pluginId === 'core.audio.effects' && p.roles.includes('owner')), true);
|
||||
assert.equal(diagnostics['audio-effects'].schema, 'slopsmith.audio_effects.diagnostics.v1');
|
||||
assert.equal(diagnostics['audio-effects'].schema, 'feedBack.audio_effects.diagnostics.v1');
|
||||
});
|
||||
|
||||
test('audio-effects runtime providers and executors are capability participants', async () => {
|
||||
const window = loadAudioEffects();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
|
||||
await registerProvider(api, {
|
||||
providerId: 'nam-tone',
|
||||
@@ -91,7 +91,7 @@ test('audio-effects runtime providers and executors are capability participants'
|
||||
|
||||
test('unregistering a provider drops its role/operations and clearing all removes the participant', async () => {
|
||||
const window = loadAudioEffects();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
|
||||
await registerProvider(api, {
|
||||
providerId: 'nam-tone',
|
||||
@@ -144,7 +144,7 @@ test('unregistering a provider drops its role/operations and clearing all remove
|
||||
|
||||
test('host runtime overlay preserves a plugin-declared audio-effects manifest entry', async () => {
|
||||
const window = loadAudioEffects();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
|
||||
// The plugin declares its own audio-effects participancy in its manifest before any runtime
|
||||
// registration goes through the host.
|
||||
@@ -181,7 +181,7 @@ test('host runtime overlay preserves a plugin-declared audio-effects manifest en
|
||||
|
||||
test('select-chain requires user action and records selected provider', async () => {
|
||||
const window = loadAudioEffects();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
await registerProvider(api);
|
||||
|
||||
const denied = await api.dispatch({ capability: 'audio-effects', command: 'select-chain', source: 'test', payload: { routeKey: 'desktop-main' } });
|
||||
@@ -197,7 +197,7 @@ test('select-chain requires user action and records selected provider', async ()
|
||||
|
||||
test('resolve-plan calls selected provider and returns constrained plan without storing raw payload in diagnostics', async () => {
|
||||
const window = loadAudioEffects();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
await registerProvider(api);
|
||||
await api.dispatch({ capability: 'audio-effects', command: 'select-chain', source: 'test', payload: { routeKey: 'desktop-main', authorization: 'user-action' } });
|
||||
|
||||
@@ -206,7 +206,7 @@ test('resolve-plan calls selected provider and returns constrained plan without
|
||||
const encoded = JSON.stringify(snapshot);
|
||||
|
||||
assert.equal(resolved.outcome, 'handled');
|
||||
assert.equal(resolved.payload.plan.schema, 'slopsmith.audio_effects.chain_plan.v1');
|
||||
assert.equal(resolved.payload.plan.schema, 'feedBack.audio_effects.chain_plan.v1');
|
||||
assert.equal(resolved.payload.plan.stages.length, 3);
|
||||
assert.deepEqual(JSON.parse(JSON.stringify(resolved.payload.plan.segments[0].stageBypass)), { 'pre-1': true, 'amp-1': false });
|
||||
assert.equal(snapshot.routes[0].state, 'resolved');
|
||||
@@ -218,13 +218,13 @@ test('resolve-plan calls selected provider and returns constrained plan without
|
||||
|
||||
test('resolve-plan rejects raw file paths and records fallback state', async () => {
|
||||
const window = loadAudioEffects();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
await registerProvider(api, {
|
||||
operationHandlers: {
|
||||
'chain.resolve': () => ({
|
||||
outcome: 'handled',
|
||||
plan: {
|
||||
schema: 'slopsmith.audio_effects.chain_plan.v1',
|
||||
schema: 'feedBack.audio_effects.chain_plan.v1',
|
||||
planId: 'bad-plan',
|
||||
routeKey: 'desktop-main',
|
||||
providerId: 'rig-builder',
|
||||
@@ -247,9 +247,9 @@ test('resolve-plan rejects raw file paths and records fallback state', async ()
|
||||
|
||||
test('load-plan calls trusted executor with provider-private assets without diagnostic leakage', async () => {
|
||||
const window = loadAudioEffects();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
let executorRequest = null;
|
||||
window.slopsmithDesktop = {
|
||||
window.feedBackDesktop = {
|
||||
audioEffects: {
|
||||
loadChainPlan(request) {
|
||||
executorRequest = request;
|
||||
@@ -262,7 +262,7 @@ test('load-plan calls trusted executor with provider-private assets without diag
|
||||
'chain.resolve': () => ({
|
||||
outcome: 'handled',
|
||||
plan: {
|
||||
schema: 'slopsmith.audio_effects.chain_plan.v1',
|
||||
schema: 'feedBack.audio_effects.chain_plan.v1',
|
||||
planId: 'private-plan',
|
||||
routeKey: 'desktop-main',
|
||||
providerId: 'rig-builder',
|
||||
@@ -303,9 +303,9 @@ test('load-plan calls trusted executor with provider-private assets without diag
|
||||
|
||||
test('route gain and release delegate to the selected executor', async () => {
|
||||
const window = loadAudioEffects();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
const calls = [];
|
||||
window.slopsmithDesktop = {
|
||||
window.feedBackDesktop = {
|
||||
audioEffects: {
|
||||
loadChainPlan() { calls.push(['load']); return { outcome: 'handled', status: 'loaded', payload: { slotsLoaded: 1 } }; },
|
||||
setRouteGain(request) { calls.push(['gain', request.gains]); return { outcome: 'handled', payload: { gains: request.gains } }; },
|
||||
@@ -314,9 +314,9 @@ test('route gain and release delegate to the selected executor', async () => {
|
||||
};
|
||||
await registerProvider(api);
|
||||
const loaded = await api.dispatch({ capability: 'audio-effects', command: 'load-plan', source: 'test', payload: { routeKey: 'desktop-main', authorization: 'playback-session' } });
|
||||
const gained = await window.slopsmith.audioEffects.setRouteGain({ routeKey: 'desktop-main', authorization: 'playback-session', gains: { input: 3, chain: 2 } });
|
||||
const released = await window.slopsmith.audioEffects.releaseRoute({ routeKey: 'desktop-main', authorization: 'playback-session' });
|
||||
const inspected = await window.slopsmith.audioEffects.inspectRoute({ routeKey: 'desktop-main' });
|
||||
const gained = await window.feedBack.audioEffects.setRouteGain({ routeKey: 'desktop-main', authorization: 'playback-session', gains: { input: 3, chain: 2 } });
|
||||
const released = await window.feedBack.audioEffects.releaseRoute({ routeKey: 'desktop-main', authorization: 'playback-session' });
|
||||
const inspected = await window.feedBack.audioEffects.inspectRoute({ routeKey: 'desktop-main' });
|
||||
|
||||
assert.equal(loaded.outcome, 'handled');
|
||||
assert.equal(gained.outcome, 'handled');
|
||||
@@ -327,8 +327,8 @@ test('route gain and release delegate to the selected executor', async () => {
|
||||
|
||||
test('provider unregister clears route plan and executor state', async () => {
|
||||
const window = loadAudioEffects();
|
||||
const api = window.slopsmith.capabilities;
|
||||
window.slopsmithDesktop = {
|
||||
const api = window.feedBack.capabilities;
|
||||
window.feedBackDesktop = {
|
||||
audioEffects: {
|
||||
loadChainPlan() { return { outcome: 'handled', status: 'loaded', payload: { slotsLoaded: 1 } }; },
|
||||
},
|
||||
@@ -360,7 +360,7 @@ test('provider unregister clears route plan and executor state', async () => {
|
||||
|
||||
test('registry caps still allow provider and executor refreshes', async () => {
|
||||
const window = loadAudioEffects();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
for (let i = 0; i < 50; i++) {
|
||||
const result = await registerProvider(api, { providerId: `provider-${i}`, pluginId: `plugin-${i}` });
|
||||
assert.equal(result.outcome, 'handled');
|
||||
@@ -400,8 +400,8 @@ test('registry caps still allow provider and executor refreshes', async () => {
|
||||
|
||||
test('route restore preserves loaded state when an executor remains active', async () => {
|
||||
const window = loadAudioEffects();
|
||||
const api = window.slopsmith.capabilities;
|
||||
window.slopsmithDesktop = {
|
||||
const api = window.feedBack.capabilities;
|
||||
window.feedBackDesktop = {
|
||||
audioEffects: {
|
||||
loadChainPlan() { return { outcome: 'handled', status: 'loaded', payload: { slotsLoaded: 1 } }; },
|
||||
},
|
||||
@@ -419,7 +419,7 @@ test('route restore preserves loaded state when an executor remains active', asy
|
||||
|
||||
test('load-plan uses a registered compatible executor when Desktop is unavailable', async () => {
|
||||
const window = loadAudioEffects();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
let executorRequest = null;
|
||||
await registerProvider(api, {
|
||||
providerId: 'nam-tone',
|
||||
@@ -428,7 +428,7 @@ test('load-plan uses a registered compatible executor when Desktop is unavailabl
|
||||
'chain.resolve': request => ({
|
||||
outcome: 'handled',
|
||||
plan: {
|
||||
schema: 'slopsmith.audio_effects.chain_plan.v1',
|
||||
schema: 'feedBack.audio_effects.chain_plan.v1',
|
||||
planId: 'browser-plan',
|
||||
routeKey: request.routeKey,
|
||||
providerId: 'nam-tone',
|
||||
@@ -482,7 +482,7 @@ test('load-plan uses a registered compatible executor when Desktop is unavailabl
|
||||
|
||||
test('load-plan redacts circular executor payloads without overflowing', async () => {
|
||||
const window = loadAudioEffects();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
const circular = { loaded: true };
|
||||
circular.self = circular;
|
||||
await registerProvider(api);
|
||||
@@ -516,7 +516,7 @@ test('load-plan redacts circular executor payloads without overflowing', async (
|
||||
|
||||
test('load-plan does not use an executor registered for a different provider', async () => {
|
||||
const window = loadAudioEffects();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
await registerProvider(api, {
|
||||
providerId: 'rig-builder',
|
||||
pluginId: 'rig_builder',
|
||||
@@ -524,7 +524,7 @@ test('load-plan does not use an executor registered for a different provider', a
|
||||
'chain.resolve': () => ({
|
||||
outcome: 'handled',
|
||||
plan: {
|
||||
schema: 'slopsmith.audio_effects.chain_plan.v1',
|
||||
schema: 'feedBack.audio_effects.chain_plan.v1',
|
||||
planId: 'rig-plan',
|
||||
routeKey: 'desktop-main',
|
||||
providerId: 'rig-builder',
|
||||
@@ -562,7 +562,7 @@ test('load-plan does not use an executor registered for a different provider', a
|
||||
|
||||
test('load-plan rejects executors that cannot support the resolved stage kinds', async () => {
|
||||
const window = loadAudioEffects();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
let called = false;
|
||||
await registerProvider(api, {
|
||||
providerId: 'rig-builder',
|
||||
@@ -571,7 +571,7 @@ test('load-plan rejects executors that cannot support the resolved stage kinds',
|
||||
'chain.resolve': () => ({
|
||||
outcome: 'handled',
|
||||
plan: {
|
||||
schema: 'slopsmith.audio_effects.chain_plan.v1',
|
||||
schema: 'feedBack.audio_effects.chain_plan.v1',
|
||||
planId: 'rig-vst-plan',
|
||||
routeKey: 'desktop-main',
|
||||
providerId: 'rig-builder',
|
||||
@@ -611,7 +611,7 @@ test('load-plan rejects executors that cannot support the resolved stage kinds',
|
||||
|
||||
test('load-plan can fall back to a compatible provider executor when the selected provider has none', async () => {
|
||||
const window = loadAudioEffects();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
let loadedProviderId = '';
|
||||
await registerProvider(api, {
|
||||
providerId: 'rig-builder',
|
||||
@@ -621,7 +621,7 @@ test('load-plan can fall back to a compatible provider executor when the selecte
|
||||
'chain.resolve': () => ({
|
||||
outcome: 'handled',
|
||||
plan: {
|
||||
schema: 'slopsmith.audio_effects.chain_plan.v1',
|
||||
schema: 'feedBack.audio_effects.chain_plan.v1',
|
||||
planId: 'rig-plan',
|
||||
routeKey: 'desktop-main',
|
||||
providerId: 'rig-builder',
|
||||
@@ -638,7 +638,7 @@ test('load-plan can fall back to a compatible provider executor when the selecte
|
||||
'chain.resolve': () => ({
|
||||
outcome: 'handled',
|
||||
plan: {
|
||||
schema: 'slopsmith.audio_effects.chain_plan.v1',
|
||||
schema: 'feedBack.audio_effects.chain_plan.v1',
|
||||
planId: 'nam-plan',
|
||||
routeKey: 'desktop-main',
|
||||
providerId: 'nam-tone',
|
||||
@@ -678,7 +678,7 @@ test('load-plan can fall back to a compatible provider executor when the selecte
|
||||
|
||||
test('load-plan can fall back from a selected provider when its executor cannot load the plan', async () => {
|
||||
const window = loadAudioEffects();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
let loadedProviderId = '';
|
||||
await registerProvider(api, {
|
||||
providerId: 'rig-builder',
|
||||
@@ -688,7 +688,7 @@ test('load-plan can fall back from a selected provider when its executor cannot
|
||||
'chain.resolve': () => ({
|
||||
outcome: 'handled',
|
||||
plan: {
|
||||
schema: 'slopsmith.audio_effects.chain_plan.v1',
|
||||
schema: 'feedBack.audio_effects.chain_plan.v1',
|
||||
planId: 'rig-vst-plan',
|
||||
routeKey: 'desktop-main',
|
||||
providerId: 'rig-builder',
|
||||
@@ -705,7 +705,7 @@ test('load-plan can fall back from a selected provider when its executor cannot
|
||||
'chain.resolve': () => ({
|
||||
outcome: 'handled',
|
||||
plan: {
|
||||
schema: 'slopsmith.audio_effects.chain_plan.v1',
|
||||
schema: 'feedBack.audio_effects.chain_plan.v1',
|
||||
planId: 'nam-plan',
|
||||
routeKey: 'desktop-main',
|
||||
providerId: 'nam-tone',
|
||||
@@ -759,14 +759,14 @@ test('load-plan can fall back from a selected provider when its executor cannot
|
||||
|
||||
test('provider operations route stage and segment changes through selected provider', async () => {
|
||||
const window = loadAudioEffects();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
const calls = [];
|
||||
await registerProvider(api, {
|
||||
operationHandlers: {
|
||||
'chain.resolve': () => ({
|
||||
outcome: 'handled',
|
||||
plan: {
|
||||
schema: 'slopsmith.audio_effects.chain_plan.v1',
|
||||
schema: 'feedBack.audio_effects.chain_plan.v1',
|
||||
planId: 'switch-plan',
|
||||
routeKey: 'desktop-main',
|
||||
providerId: 'rig-builder',
|
||||
@@ -793,7 +793,7 @@ test('provider operations route stage and segment changes through selected provi
|
||||
|
||||
test('mapping helpers call core mapping API with provider-tagged payloads', async () => {
|
||||
const window = loadAudioEffects();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
const calls = [];
|
||||
window.fetch = async (url, options = {}) => {
|
||||
calls.push({ url: String(url), options });
|
||||
@@ -815,7 +815,7 @@ test('mapping helpers call core mapping API with provider-tagged payloads', asyn
|
||||
};
|
||||
};
|
||||
|
||||
const saved = await window.slopsmith.audioEffects.upsertMapping({
|
||||
const saved = await window.feedBack.audioEffects.upsertMapping({
|
||||
song_key: 'settings-v1-song',
|
||||
filename: 'Artist - Song_p.archive',
|
||||
tone_key: 'Dist',
|
||||
@@ -829,8 +829,8 @@ test('mapping helpers call core mapping API with provider-tagged payloads', asyn
|
||||
source: 'rig_builder',
|
||||
payload: { song_key: 'settings-v1-song', provider_id: 'rig-builder' },
|
||||
});
|
||||
const activated = await window.slopsmith.audioEffects.activateMapping({ mappingId: 7, providerId: 'rig-builder' });
|
||||
const cleared = await window.slopsmith.audioEffects.clearActiveMapping({ songKey: 'settings-v1-song', toneKey: 'Dist' });
|
||||
const activated = await window.feedBack.audioEffects.activateMapping({ mappingId: 7, providerId: 'rig-builder' });
|
||||
const cleared = await window.feedBack.audioEffects.clearActiveMapping({ songKey: 'settings-v1-song', toneKey: 'Dist' });
|
||||
|
||||
assert.equal(saved.outcome, 'handled');
|
||||
assert.equal(saved.payload.mapping.provider_ref, 'chain:99');
|
||||
@@ -856,21 +856,21 @@ test('mapping helpers forward present falsey fields to the server instead of swa
|
||||
|
||||
// A falsey non-string provider_id must reach the server (which rejects it) rather than being
|
||||
// coerced to '' client-side, which would become a silent unscoped activate.
|
||||
await window.slopsmith.audioEffects.activateMapping({ mappingId: 7, providerId: false });
|
||||
await window.feedBack.audioEffects.activateMapping({ mappingId: 7, providerId: false });
|
||||
assert.equal(JSON.parse(calls[0].options.body).provider_id, false);
|
||||
|
||||
// A present falsey query filter is forwarded (stringified) rather than dropped.
|
||||
await window.slopsmith.audioEffects.listMappings({ provider_id: 0 });
|
||||
await window.feedBack.audioEffects.listMappings({ provider_id: 0 });
|
||||
assert.match(calls[1].url, /provider_id=0/);
|
||||
|
||||
// An omitted filter stays omitted (no spurious empty filter).
|
||||
await window.slopsmith.audioEffects.listMappings({});
|
||||
await window.feedBack.audioEffects.listMappings({});
|
||||
assert.equal(calls[2].url, '/api/audio-effects/mappings');
|
||||
});
|
||||
|
||||
test('bridge hits are safe and diagnosable', async () => {
|
||||
const window = loadAudioEffects();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
|
||||
const result = await api.dispatch({
|
||||
capability: 'audio-effects',
|
||||
@@ -902,7 +902,7 @@ test('bridge hits are safe and diagnosable', async () => {
|
||||
routeKey: 'desktop-main',
|
||||
bridgeId: 'audio-effects.legacy-native-load',
|
||||
pluginId: 'nam_tone',
|
||||
legacySurface: 'window.slopsmithDesktop.audio.loadPreset /Users/example/model.nam',
|
||||
legacySurface: 'window.feedBackDesktop.audio.loadPreset /Users/example/model.nam',
|
||||
},
|
||||
});
|
||||
const encoded = JSON.stringify(diagnosticsSnapshot(window));
|
||||
@@ -910,7 +910,7 @@ test('bridge hits are safe and diagnosable', async () => {
|
||||
assert.equal(result.outcome, 'handled');
|
||||
assert.equal(dbResult.outcome, 'handled');
|
||||
assert.equal(nativeResult.outcome, 'handled');
|
||||
const sharedShim = window.slopsmith.capabilities.snapshotDiagnostics().compatibilityShims.find(entry => entry.shimId === 'audio-effects.legacy-nam-routing');
|
||||
const sharedShim = window.feedBack.capabilities.snapshotDiagnostics().compatibilityShims.find(entry => entry.shimId === 'audio-effects.legacy-nam-routing');
|
||||
assert.equal(sharedShim.status, 'used');
|
||||
assert.equal(sharedShim.hitCount >= 1, true);
|
||||
assert.equal(encoded.includes('audio-effects.legacy-nam-routing'), true);
|
||||
|
||||
@@ -16,7 +16,7 @@ function loadAudioEffects(options = {}) {
|
||||
}
|
||||
|
||||
function diagnosticsSnapshot(window) {
|
||||
return window.slopsmith.audioEffects.snapshot();
|
||||
return window.feedBack.audioEffects.snapshot();
|
||||
}
|
||||
|
||||
module.exports = { loadAudioEffects, diagnosticsSnapshot, ROOT };
|
||||
|
||||
@@ -26,7 +26,7 @@ test('legacy fader API remains compatible while bridge hits are attributed', asy
|
||||
runBrowserScript(window, 'static/audio-mixer.js');
|
||||
|
||||
let volume = 0.5;
|
||||
window.slopsmith.audio.registerFader({
|
||||
window.feedBack.audio.registerFader({
|
||||
id: 'plugin.delay',
|
||||
label: 'Delay',
|
||||
min: 0,
|
||||
@@ -37,8 +37,8 @@ test('legacy fader API remains compatible while bridge hits are attributed', asy
|
||||
setValue: value => { volume = value; },
|
||||
});
|
||||
|
||||
const snapshot = window.slopsmith.audioSession.snapshot();
|
||||
assert.equal(window.slopsmith.audio.getFaders().some(fader => fader.id === 'plugin.delay'), true);
|
||||
const snapshot = window.feedBack.audioSession.snapshot();
|
||||
assert.equal(window.feedBack.audio.getFaders().some(fader => fader.id === 'plugin.delay'), true);
|
||||
assert.equal(snapshot.domains['audio-mix'].participants.some(participant => participant.participantId === 'fader.plugin.delay'), true);
|
||||
assert.equal(snapshot.domains['audio-mix'].bridges.some(bridge => bridge.bridgeId === 'audio-mix.fader-registry'), true);
|
||||
});
|
||||
@@ -48,9 +48,9 @@ test('legacy analyser fallback records bridge status without losing analyser out
|
||||
installAnalyserDom(window);
|
||||
runBrowserScript(window, 'plugins/highway_3d/screen.js');
|
||||
|
||||
const analyser = window.slopsmithViz_highway_3d.__test.getAnalyserForBridgeTest();
|
||||
const bands = window.slopsmithViz_highway_3d.__test.readBandsForBridgeTest();
|
||||
const bridge = window.slopsmith.audioSession.snapshot().domains['audio-mix'].bridges.find(entry => entry.bridgeId === 'audio-mix.analyser');
|
||||
const analyser = window.feedBackViz_highway_3d.__test.getAnalyserForBridgeTest();
|
||||
const bands = window.feedBackViz_highway_3d.__test.readBandsForBridgeTest();
|
||||
const bridge = window.feedBack.audioSession.snapshot().domains['audio-mix'].bridges.find(entry => entry.bridgeId === 'audio-mix.analyser');
|
||||
|
||||
assert.equal(analyser.source, 'core');
|
||||
assert.equal(bands.bass > 0, true);
|
||||
@@ -59,9 +59,9 @@ test('legacy analyser fallback records bridge status without losing analyser out
|
||||
|
||||
test('barrier and input compatibility surfaces are visible in diagnostics', () => {
|
||||
const window = loadAudioSession();
|
||||
const audioSession = window.slopsmith.audioSession;
|
||||
const audioSession = window.feedBack.audioSession;
|
||||
|
||||
audioSession.recordBridgeHit({ domain: 'audio-monitoring', bridgeId: 'audio-monitoring.audio-barrier', legacySurface: 'window.slopsmithAudioBarrier', participantId: 'note_detect', outcome: 'degraded', reason: 'timeout' });
|
||||
audioSession.recordBridgeHit({ domain: 'audio-monitoring', bridgeId: 'audio-monitoring.audio-barrier', legacySurface: 'window.feedBackAudioBarrier', participantId: 'note_detect', outcome: 'degraded', reason: 'timeout' });
|
||||
audioSession.recordBridgeHit({ domain: 'audio-input', bridgeId: 'audio-input.legacy-source', legacySurface: 'navigator.mediaDevices.getUserMedia', participantId: 'note_detect', outcome: 'denied', reason: 'permission denied' });
|
||||
|
||||
const snapshot = audioSession.snapshot();
|
||||
@@ -71,7 +71,7 @@ test('barrier and input compatibility surfaces are visible in diagnostics', () =
|
||||
|
||||
test('a legacy bridge hit with unsafe fields never leaks a path/token into diagnostics', () => {
|
||||
const window = loadAudioSession();
|
||||
const audioSession = window.slopsmith.audioSession;
|
||||
const audioSession = window.feedBack.audioSession;
|
||||
|
||||
audioSession.recordBridgeHit({
|
||||
domain: 'audio-input',
|
||||
@@ -91,7 +91,7 @@ test('a legacy bridge hit with unsafe fields never leaks a path/token into diagn
|
||||
|
||||
test('audio-input explicit enumeration registers provider sources without list prompting', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
const provider = makeInputProvider({
|
||||
providerId: 'desktop_audio',
|
||||
sourceId: 'bootstrap-source',
|
||||
@@ -106,7 +106,7 @@ test('audio-input explicit enumeration registers provider sources without list p
|
||||
assert.equal(listed.payload.sources.some(source => source.logicalSourceKey === 'desktop:instrument:secondary'), false);
|
||||
assert.deepEqual(provider.calls, []);
|
||||
|
||||
const enumerated = await window.slopsmith.audioSession.enumerateInputSources({ providerId: 'desktop_audio', explicit: true, requesterId: 'settings' });
|
||||
const enumerated = await window.feedBack.audioSession.enumerateInputSources({ providerId: 'desktop_audio', explicit: true, requesterId: 'settings' });
|
||||
const after = await api.dispatch({ capability: 'audio-input', command: 'list-sources', source: 'test' });
|
||||
|
||||
assert.equal(enumerated.outcome, 'handled');
|
||||
@@ -117,7 +117,7 @@ test('audio-input explicit enumeration registers provider sources without list p
|
||||
|
||||
test('audio-input native source wins over compatibility-backed duplicate', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
|
||||
await api.dispatch({
|
||||
capability: 'audio-input',
|
||||
@@ -150,7 +150,7 @@ test('audio-input native source wins over compatibility-backed duplicate', async
|
||||
});
|
||||
|
||||
const listed = await api.dispatch({ capability: 'audio-input', command: 'list-sources', source: 'test' });
|
||||
const snapshot = window.slopsmith.audioSession.snapshot().domains['audio-input'];
|
||||
const snapshot = window.feedBack.audioSession.snapshot().domains['audio-input'];
|
||||
|
||||
assert.equal(listed.payload.sources.length, 1);
|
||||
assert.equal(listed.payload.sources[0].providerId, 'native_input');
|
||||
@@ -160,7 +160,7 @@ test('audio-input native source wins over compatibility-backed duplicate', async
|
||||
|
||||
test('audio-monitoring native provider wins over legacy compatibility provider and overshadows its compatibility bridge', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
const legacy = makeMonitoringProvider({
|
||||
providerId: 'legacy_monitor',
|
||||
logicalMonitoringKey: 'shared:monitor:primary',
|
||||
@@ -177,7 +177,7 @@ test('audio-monitoring native provider wins over legacy compatibility provider a
|
||||
await api.dispatch({ capability: 'audio-monitoring', command: 'register-provider', source: 'native_monitor', payload: native.provider });
|
||||
|
||||
const listed = await api.dispatch({ capability: 'audio-monitoring', command: 'list-providers', source: 'test' });
|
||||
const snapshot = window.slopsmith.audioSession.snapshot().domains['audio-monitoring'];
|
||||
const snapshot = window.feedBack.audioSession.snapshot().domains['audio-monitoring'];
|
||||
|
||||
// The compatibility bridge below is the one the registration/supersession path actually produces;
|
||||
// asserting only on it (not on manually pre-seeded bridge hits) keeps this test honest if the
|
||||
@@ -192,10 +192,10 @@ test('legacy registerFader callbacks are usable through audio-mix get and set op
|
||||
const window = loadAudioSession();
|
||||
installMixerDom(window);
|
||||
runBrowserScript(window, 'static/audio-mixer.js');
|
||||
window.slopsmith.audioSession.startSession({ sessionId: 'main:test-song' });
|
||||
window.feedBack.audioSession.startSession({ sessionId: 'main:test-song' });
|
||||
|
||||
let gain = 0.35;
|
||||
window.slopsmith.audio.registerFader({
|
||||
window.feedBack.audio.registerFader({
|
||||
id: 'plugin.gain',
|
||||
label: 'Plugin Gain',
|
||||
min: 0,
|
||||
@@ -206,7 +206,7 @@ test('legacy registerFader callbacks are usable through audio-mix get and set op
|
||||
setValue: value => { gain = Math.min(0.9, value); return gain; },
|
||||
});
|
||||
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
const listed = await api.dispatch({ capability: 'audio-mix', command: 'list-faders', source: 'test' });
|
||||
const read = await api.dispatch({ capability: 'audio-mix', command: 'get-fader-value', source: 'test', payload: { participantId: 'fader.plugin.gain', faderId: 'plugin.gain' } });
|
||||
const written = await api.dispatch({ capability: 'audio-mix', command: 'set-fader-value', source: 'test', payload: { participantId: 'fader.plugin.gain', faderId: 'plugin.gain', value: 1 } });
|
||||
@@ -216,7 +216,7 @@ test('legacy registerFader callbacks are usable through audio-mix get and set op
|
||||
assert.equal(written.payload.committedValue, 0.9);
|
||||
assert.equal(gain, 0.9);
|
||||
|
||||
window.slopsmith.audio.unregisterFader('plugin.gain');
|
||||
assert.equal(window.slopsmith.audio.getFaders().some(fader => fader.id === 'plugin.gain'), false);
|
||||
assert.equal(window.slopsmith.audioSession.snapshot().domains['audio-mix'].participants.some(participant => participant.participantId === 'fader.plugin.gain'), false);
|
||||
window.feedBack.audio.unregisterFader('plugin.gain');
|
||||
assert.equal(window.feedBack.audio.getFaders().some(fader => fader.id === 'plugin.gain'), false);
|
||||
assert.equal(window.feedBack.audioSession.snapshot().domains['audio-mix'].participants.some(participant => participant.participantId === 'fader.plugin.gain'), false);
|
||||
});
|
||||
@@ -4,8 +4,8 @@ const { loadAudioSession, diagnosticsSnapshot, makeInputProvider, makeMonitoring
|
||||
|
||||
test('audio session host registers active core domains and contributes diagnostics', () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const diagnostics = window.slopsmith.diagnostics.snapshotContributions();
|
||||
const api = window.feedBack.capabilities;
|
||||
const diagnostics = window.feedBack.diagnostics.snapshotContributions();
|
||||
|
||||
for (const domain of ['audio-mix', 'audio-input', 'audio-monitoring']) {
|
||||
const pipeline = api.inspect(domain);
|
||||
@@ -15,12 +15,12 @@ test('audio session host registers active core domains and contributes diagnosti
|
||||
const stemsPipeline = api.inspect('stems');
|
||||
assert.equal(stemsPipeline.review.lifecycle, 'active');
|
||||
assert.equal(stemsPipeline.participants.some(p => p.pluginId === 'core.audio.session' && p.roles.includes('coordinator') && !p.roles.includes('owner')), true);
|
||||
assert.equal(diagnostics['audio-session'].schema, 'slopsmith.audio_session.diagnostics.v1');
|
||||
assert.equal(diagnostics['audio-session'].schema, 'feedBack.audio_session.diagnostics.v1');
|
||||
});
|
||||
|
||||
test('audio session lifecycle and snapshots redact source identity with per-snapshot pseudonyms', () => {
|
||||
const window = loadAudioSession();
|
||||
const audioSession = window.slopsmith.audioSession;
|
||||
const audioSession = window.feedBack.audioSession;
|
||||
|
||||
audioSession.startSession({ sessionId: 'main:/Users/example/DLC/song.archive', songKey: '/Users/example/DLC/song.archive', songFormat: 'archive' });
|
||||
audioSession.setRoute({ routeKind: 'html5', availability: 'available', deviceLabel: 'Scarlett 2i2 Serial 1234' });
|
||||
@@ -36,7 +36,7 @@ test('audio session lifecycle and snapshots redact source identity with per-snap
|
||||
|
||||
test('audio-input diagnostics redact source ids labels handles and bounded reasons', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
|
||||
await api.dispatch({
|
||||
capability: 'audio-input',
|
||||
@@ -72,7 +72,7 @@ test('audio-input diagnostics redact source ids labels handles and bounded reaso
|
||||
|
||||
test('audio-input shares compatible open sessions and closes provider after last release', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
const calls = [];
|
||||
|
||||
await api.dispatch({
|
||||
@@ -110,7 +110,7 @@ test('audio-input shares compatible open sessions and closes provider after last
|
||||
|
||||
test('audio diagnostics record bounded runtime outcomes and domain statuses', () => {
|
||||
const window = loadAudioSession();
|
||||
const audioSession = window.slopsmith.audioSession;
|
||||
const audioSession = window.feedBack.audioSession;
|
||||
|
||||
for (let i = 0; i < 120; i += 1) {
|
||||
audioSession.recordOutcome({ domain: 'audio-input', operation: 'select-source', participantId: 'test', outcome: 'degraded', status: 'unavailable', reason: `missing-${i}` });
|
||||
@@ -124,8 +124,8 @@ test('audio diagnostics record bounded runtime outcomes and domain statuses', ()
|
||||
|
||||
test('disabled missing incompatible unsupported and timeout paths are diagnosable', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const audioSession = window.slopsmith.audioSession;
|
||||
const api = window.feedBack.capabilities;
|
||||
const audioSession = window.feedBack.audioSession;
|
||||
|
||||
audioSession.registerMixParticipant({ participantId: 'disabled-fader', availability: 'disabled' });
|
||||
assert.equal(audioSession.snapshot().domains['audio-mix'].participants[0].availability, 'disabled');
|
||||
@@ -154,8 +154,8 @@ test('disabled missing incompatible unsupported and timeout paths are diagnosabl
|
||||
|
||||
test('audio-mix diagnostics include faders routes analysers bridge hits and redacted outcomes', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const audioSession = window.slopsmith.audioSession;
|
||||
const api = window.feedBack.capabilities;
|
||||
const audioSession = window.feedBack.audioSession;
|
||||
audioSession.startSession({ sessionId: 'main:/Users/example/DLC/song.archive', songKey: '/Users/example/DLC/song.archive' });
|
||||
audioSession.setRoute({ routeKind: 'desktop', availability: 'degraded', deviceLabel: 'Secret Studio Output', fallbackReason: 'fallback token=abc123 at /Users/example/device' });
|
||||
audioSession.setAnalyser({ source: 'plugin', availability: 'available', participantId: 'plugin.visualizer', reason: 'ok', rawFft: [1, 2, 3] });
|
||||
@@ -197,7 +197,7 @@ test('audio-mix diagnostics include faders routes analysers bridge hits and reda
|
||||
|
||||
test('audio-monitoring diagnostics redact provider source session handles and private payloads', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
const input = makeInputProvider({
|
||||
providerId: 'secret_input',
|
||||
sourceId: 'USB Interface Hardware ABC1234',
|
||||
@@ -228,7 +228,7 @@ test('audio-monitoring diagnostics redact provider source session handles and pr
|
||||
delete providerPayload.safeLabel;
|
||||
await api.dispatch({ capability: 'audio-monitoring', command: 'register-provider', source: monitoring.provider.providerId, payload: providerPayload });
|
||||
const started = await api.dispatch({ capability: 'audio-monitoring', command: 'start', source: 'user', payload: { requesterId: 'user', authorization: 'user-action', requiredChannelShape: 'mono' } });
|
||||
window.slopsmith.audioSession.recordOutcome({ domain: 'audio-monitoring', operation: 'start', participantId: 'secret_monitor', providerId: 'secret_monitor', monitoringId: started.payload.monitoringId, sourceId: 'USB Interface Hardware ABC1234', openSessionId: 'open raw id 1234', requesterId: 'user', outcome: 'failed', status: 'timeout', reason: 'failed at /Users/barlind/private secret=abc123' });
|
||||
window.feedBack.audioSession.recordOutcome({ domain: 'audio-monitoring', operation: 'start', participantId: 'secret_monitor', providerId: 'secret_monitor', monitoringId: started.payload.monitoringId, sourceId: 'USB Interface Hardware ABC1234', openSessionId: 'open raw id 1234', requesterId: 'user', outcome: 'failed', status: 'timeout', reason: 'failed at /Users/barlind/private secret=abc123' });
|
||||
|
||||
const snapshot = diagnosticsSnapshot(window);
|
||||
const encoded = JSON.stringify(snapshot);
|
||||
|
||||
@@ -26,7 +26,7 @@ async function registerSource(api, payload = {}) {
|
||||
|
||||
test('audio-input requires sourceId providerId and logicalSourceKey', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
|
||||
const missingSource = await api.dispatch({ capability: 'audio-input', command: 'register-source', source: 'test', payload: { providerId: 'test', logicalSourceKey: 'test:key' } });
|
||||
const missingProvider = await api.dispatch({ capability: 'audio-input', command: 'register-source', source: 'test', payload: { sourceId: 'source-1', logicalSourceKey: 'test:key' } });
|
||||
@@ -41,7 +41,7 @@ test('audio-input requires sourceId providerId and logicalSourceKey', async () =
|
||||
|
||||
test('audio-input registration list inspect select and snapshots pseudonymize source identity', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
const registeredEvents = captureEvents(window, 'audio-input:source-registered');
|
||||
const selectedEvents = captureEvents(window, 'audio-input:source-selected');
|
||||
|
||||
@@ -62,7 +62,7 @@ test('audio-input registration list inspect select and snapshots pseudonymize so
|
||||
assert.equal(registeredEvents.length, 1);
|
||||
assert.equal(selectedEvents.length, 1);
|
||||
|
||||
const encoded = JSON.stringify(window.slopsmith.audioSession.snapshot().domains['audio-input']);
|
||||
const encoded = JSON.stringify(window.feedBack.audioSession.snapshot().domains['audio-input']);
|
||||
assert.equal(encoded.includes('source-raw-id-12345'), false);
|
||||
assert.equal(encoded.includes('Scarlett'), false);
|
||||
assert.equal(encoded.includes('987654'), false);
|
||||
@@ -70,7 +70,7 @@ test('audio-input registration list inspect select and snapshots pseudonymize so
|
||||
|
||||
test('audio-input pseudonyms are per-bundle: distinct within a snapshot, never leak raw identity', () => {
|
||||
const window = loadAudioSession();
|
||||
const audioSession = window.slopsmith.audioSession;
|
||||
const audioSession = window.feedBack.audioSession;
|
||||
|
||||
audioSession.registerInputSource({ sourceId: 'mic-A', providerId: 'note_detect', logicalSourceKey: 'note_detect:mic-a', label: '/Users/me/My Songs/mic A' });
|
||||
audioSession.registerInputSource({ sourceId: 'mic-B', providerId: 'note_detect', logicalSourceKey: 'note_detect:mic-b', label: 'device B' });
|
||||
@@ -90,7 +90,7 @@ test('audio-input pseudonyms are per-bundle: distinct within a snapshot, never l
|
||||
|
||||
test('audio-input degraded select and unknown unregister never leak the raw source id', () => {
|
||||
const window = loadAudioSession();
|
||||
const audioSession = window.slopsmith.audioSession;
|
||||
const audioSession = window.feedBack.audioSession;
|
||||
|
||||
const degraded = audioSession.selectInputSource('/Users/me/secret-device', 'note_detect');
|
||||
const removed = audioSession.unregisterInputSource('/Users/me/secret-device');
|
||||
@@ -107,7 +107,7 @@ test('audio-input degraded select and unknown unregister never leak the raw sour
|
||||
|
||||
test('unknown unregister echoes the requested logicalSourceKey/providerId without pseudonymizing them', () => {
|
||||
const window = loadAudioSession();
|
||||
const audioSession = window.slopsmith.audioSession;
|
||||
const audioSession = window.feedBack.audioSession;
|
||||
|
||||
const removed = audioSession.unregisterInputSource({ logicalSourceKey: 'note_detect:instrument:primary', providerId: 'note_detect' });
|
||||
|
||||
@@ -121,7 +121,7 @@ test('unknown unregister echoes the requested logicalSourceKey/providerId withou
|
||||
|
||||
test('unregister-source uses providerId to target the right source among logical-key duplicates', () => {
|
||||
const window = loadAudioSession();
|
||||
const audioSession = window.slopsmith.audioSession;
|
||||
const audioSession = window.feedBack.audioSession;
|
||||
|
||||
audioSession.registerInputSource({ sourceId: 'native-x', logicalSourceKey: 'dup:disambig', providerId: 'native_p', kind: 'instrument', safeLabel: 'N' });
|
||||
audioSession.registerInputSource({ sourceId: 'compat-x', logicalSourceKey: 'dup:disambig', providerId: 'compat_p', compatibilitySource: 'legacy', kind: 'instrument', safeLabel: 'C' });
|
||||
@@ -136,7 +136,7 @@ test('unregister-source uses providerId to target the right source among logical
|
||||
|
||||
test('register-source rejects a sourceId already owned by another provider', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
|
||||
const first = await api.dispatch({ capability: 'audio-input', command: 'register-source', source: 'prov_a', payload: { sourceId: 'shared-id', logicalSourceKey: 'a:key', providerId: 'prov_a' } });
|
||||
const collision = await api.dispatch({ capability: 'audio-input', command: 'register-source', source: 'prov_b', payload: { sourceId: 'shared-id', logicalSourceKey: 'b:key', providerId: 'prov_b' } });
|
||||
@@ -153,20 +153,20 @@ test('register-source rejects a sourceId already owned by another provider', asy
|
||||
|
||||
test('register-source rejects a logicalSourceKey that is not redaction-safe', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
|
||||
const result = await api.dispatch({ capability: 'audio-input', command: 'register-source', source: 'p', payload: { sourceId: 's1', providerId: 'p', logicalSourceKey: '/Users/me/secret token=abc123' } });
|
||||
|
||||
assert.equal(result.outcome, 'failed');
|
||||
// The unsafe key must not be stored or leak into diagnostics.
|
||||
const encoded = JSON.stringify(window.slopsmith.audioSession.snapshot());
|
||||
const encoded = JSON.stringify(window.feedBack.audioSession.snapshot());
|
||||
assert.equal(encoded.includes('/Users/me'), false);
|
||||
assert.equal(encoded.includes('token=abc123'), false);
|
||||
});
|
||||
|
||||
test('enumerate denied with an unsafe providerId never leaks it into diagnostics', async () => {
|
||||
const window = loadAudioSession();
|
||||
const audioSession = window.slopsmith.audioSession;
|
||||
const audioSession = window.feedBack.audioSession;
|
||||
|
||||
// Missing explicit/userInitiated -> denied; the caller-supplied providerId is unsafe and is
|
||||
// recorded as the outcome participantId/providerId, so it must be bounded in the snapshot.
|
||||
@@ -182,20 +182,20 @@ test('enumerate denied with an unsafe providerId never leaks it into diagnostics
|
||||
|
||||
test('a malicious dispatch source is redacted before becoming a requesterId in diagnostics', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
|
||||
await registerSource(api, { sourceId: 'req-raw', logicalSourceKey: 'test:req' });
|
||||
// The capability dispatch `source` (the requester identity) is attacker-controlled here.
|
||||
await api.dispatch({ capability: 'audio-input', command: 'select-source', source: '/Users/me/plugin token=abc123', payload: { logicalSourceKey: 'test:req' } });
|
||||
|
||||
const encoded = JSON.stringify(window.slopsmith.audioSession.snapshot());
|
||||
const encoded = JSON.stringify(window.feedBack.audioSession.snapshot());
|
||||
assert.equal(encoded.includes('/Users/me'), false);
|
||||
assert.equal(encoded.includes('abc123'), false);
|
||||
});
|
||||
|
||||
test('caller-provided logical keys are bounded so a path/token cannot leak into diagnostics on a miss', () => {
|
||||
const window = loadAudioSession();
|
||||
const audioSession = window.slopsmith.audioSession;
|
||||
const audioSession = window.feedBack.audioSession;
|
||||
|
||||
// Untrusted callers may pass an unsafe value as a logical key; select/unregister misses must not
|
||||
// echo or record it raw into the redaction-safe diagnostics snapshot.
|
||||
@@ -211,7 +211,7 @@ test('caller-provided logical keys are bounded so a path/token cannot leak into
|
||||
|
||||
test('audio-monitoring distinguishes a failed state from transient unavailability', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
|
||||
await registerSource(api);
|
||||
await api.dispatch({ capability: 'audio-input', command: 'select-source', source: 'user', payload: { logicalSourceKey: 'test:instrument:primary' } });
|
||||
@@ -231,7 +231,7 @@ test('audio-monitoring distinguishes a failed state from transient unavailabilit
|
||||
|
||||
test('inspect list and select do not call provider enumeration or open handlers', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
const provider = makeInputProvider({ providerId: 'note_detect', logicalSourceKey: 'note_detect:instrument:primary' });
|
||||
|
||||
await registerSource(api, provider.source);
|
||||
@@ -244,7 +244,7 @@ test('inspect list and select do not call provider enumeration or open handlers'
|
||||
|
||||
test('open-source and close-source record outcomes events and no live handles', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
const openedEvents = captureEvents(window, 'audio-input:source-opened');
|
||||
const closedEvents = captureEvents(window, 'audio-input:source-closed');
|
||||
const degradedEvents = captureEvents(window, 'audio-input:source-open-degraded');
|
||||
@@ -275,7 +275,7 @@ test('open-source and close-source record outcomes events and no live handles',
|
||||
assert.equal(incompatible.payload.state, 'incompatible');
|
||||
assert.equal(degradedEvents.length >= 1, true);
|
||||
|
||||
const encoded = JSON.stringify(window.slopsmith.audioSession.snapshot());
|
||||
const encoded = JSON.stringify(window.feedBack.audioSession.snapshot());
|
||||
assert.equal(encoded.includes('mediaStream'), false);
|
||||
assert.equal(encoded.includes('nativeHandle'), false);
|
||||
assert.equal(encoded.includes('token=abc'), false);
|
||||
@@ -283,7 +283,7 @@ test('open-source and close-source record outcomes events and no live handles',
|
||||
|
||||
test('open-source reports no-owner no-handler unsupported failed and malformed provider data distinctly', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
|
||||
const noOwner = await api.dispatch({ capability: 'audio-input', command: 'open-source', source: 'note_detect', payload: { requesterId: 'note_detect' } });
|
||||
assert.equal(noOwner.outcome, 'no-owner');
|
||||
@@ -308,7 +308,7 @@ test('open-source reports no-owner no-handler unsupported failed and malformed p
|
||||
const malformed = await api.dispatch({ capability: 'audio-input', command: 'open-source', source: 'note_detect', payload: { requesterId: 'note_detect' } });
|
||||
assert.equal(malformed.outcome, 'handled');
|
||||
|
||||
const outcomes = window.slopsmith.audioSession.snapshot().recentOutcomes.filter(outcome => outcome.domain === 'audio-input');
|
||||
const outcomes = window.feedBack.audioSession.snapshot().recentOutcomes.filter(outcome => outcome.domain === 'audio-input');
|
||||
assert.equal(outcomes.some(outcome => outcome.status === 'no-owner' || outcome.outcome === 'no-owner'), true);
|
||||
assert.equal(outcomes.some(outcome => outcome.outcome === 'unsupported-command'), true);
|
||||
assert.equal(outcomes.some(outcome => outcome.outcome === 'no-handler'), true);
|
||||
@@ -317,7 +317,7 @@ test('open-source reports no-owner no-handler unsupported failed and malformed p
|
||||
|
||||
test('open-source never switches to a non-selected source addressed by raw sourceId or logical key', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
|
||||
await registerSource(api, { sourceId: 'primary-raw', logicalSourceKey: 'test:primary' });
|
||||
await registerSource(api, { sourceId: 'other-raw', logicalSourceKey: 'test:other' });
|
||||
@@ -335,7 +335,7 @@ test('open-source never switches to a non-selected source addressed by raw sourc
|
||||
|
||||
test('open-source emits an open-session-shaped payload (with requester attribution) for a pre-denied source', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
const deniedEvents = captureEvents(window, 'audio-input:permission-denied');
|
||||
|
||||
await registerSource(api, { sourceId: 'predenied-raw', logicalSourceKey: 'test:predenied', availability: 'denied', reason: 'permission denied' });
|
||||
@@ -353,7 +353,7 @@ test('open-source emits an open-session-shaped payload (with requester attributi
|
||||
|
||||
test('open-source source-open-degraded uses an open-session-shaped payload when nothing is selected', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
const degraded = captureEvents(window, 'audio-input:source-open-degraded');
|
||||
|
||||
// No source selected -> degraded; the event must share the OpenInputSessionSummary schema.
|
||||
@@ -369,7 +369,7 @@ test('open-source source-open-degraded uses an open-session-shaped payload when
|
||||
|
||||
test('open-source reports the selected source as unavailable when no matching source is registered', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
|
||||
await registerSource(api, { sourceId: 'gone-raw', logicalSourceKey: 'test:gone' });
|
||||
await api.dispatch({ capability: 'audio-input', command: 'select-source', source: 'user', payload: { logicalSourceKey: 'test:gone' } });
|
||||
@@ -385,7 +385,7 @@ test('open-source reports the selected source as unavailable when no matching so
|
||||
|
||||
test('open-source and close-source ignore a payload requesterId so callers cannot spoof session ownership', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
|
||||
await registerSource(api, { sourceId: 'owned-raw', logicalSourceKey: 'test:owned' });
|
||||
await api.dispatch({ capability: 'audio-input', command: 'select-source', source: 'user', payload: { logicalSourceKey: 'test:owned' } });
|
||||
@@ -405,7 +405,7 @@ test('open-source and close-source ignore a payload requesterId so callers canno
|
||||
|
||||
test('open-source and close-source propagate a provider non-handled outcome exactly', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
|
||||
// source.open returns an explicit non-denied/failed outcome -> must propagate, not collapse to degraded.
|
||||
await registerSource(api, { sourceId: 'po-raw', logicalSourceKey: 'test:po', operationHandlers: {
|
||||
@@ -430,7 +430,7 @@ test('open-source and close-source propagate a provider non-handled outcome exac
|
||||
|
||||
test('close-source accepts logicalKey/sourceKey aliases like the other audio-input paths', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
|
||||
await registerSource(api, { sourceId: 'alias-raw', logicalSourceKey: 'test:alias' });
|
||||
await api.dispatch({ capability: 'audio-input', command: 'select-source', source: 'user', payload: { logicalSourceKey: 'test:alias' } });
|
||||
@@ -445,7 +445,7 @@ test('close-source accepts logicalKey/sourceKey aliases like the other audio-inp
|
||||
|
||||
test('close-source resolves by logicalSourceKey when requiredChannelShape is omitted', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
|
||||
// Default source is stereo; open without a channel-shape hint so the session is keyed by the
|
||||
// source's resolved shape, then close by logical key alone (requiredChannelShape is optional).
|
||||
@@ -461,7 +461,7 @@ test('close-source resolves by logicalSourceKey when requiredChannelShape is omi
|
||||
|
||||
test('close-source with an explicit wrong requiredChannelShape does not close a differently-shaped session', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
|
||||
// Default source supports mono+stereo; open as mono so the session is keyed by 'mono'.
|
||||
await registerSource(api, { sourceId: 'shaped-raw', logicalSourceKey: 'test:shaped' });
|
||||
@@ -472,7 +472,7 @@ test('close-source with an explicit wrong requiredChannelShape does not close a
|
||||
// An explicit, wrong shape must NOT fall back and close the mono session.
|
||||
const wrongShape = await api.dispatch({ capability: 'audio-input', command: 'close-source', source: 'note_detect', payload: { requesterId: 'note_detect', logicalSourceKey: 'test:shaped', requiredChannelShape: 'stereo' } });
|
||||
assert.equal(wrongShape.outcome, 'no-handler');
|
||||
assert.equal(window.slopsmith.audioSession.snapshot().domains['audio-input'].totalOpenSessions, 1);
|
||||
assert.equal(window.feedBack.audioSession.snapshot().domains['audio-input'].totalOpenSessions, 1);
|
||||
|
||||
// The original session still closes via openSessionId.
|
||||
const right = await api.dispatch({ capability: 'audio-input', command: 'close-source', source: 'note_detect', payload: { requesterId: 'note_detect', openSessionId: open.payload.openSessionId } });
|
||||
@@ -481,7 +481,7 @@ test('close-source with an explicit wrong requiredChannelShape does not close a
|
||||
|
||||
test('open-source rejects a non-selected duplicate sharing the logical key, addressed by sourceId', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
|
||||
// Native + compatibility-backed duplicate share one logical key; the native source wins.
|
||||
await registerSource(api, { sourceId: 'native-raw', logicalSourceKey: 'dup:key', providerId: 'native_provider' });
|
||||
@@ -500,7 +500,7 @@ test('open-source rejects a non-selected duplicate sharing the logical key, addr
|
||||
|
||||
test('inspect resolves a raw sourceId to its source via the stable logical key', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
|
||||
await registerSource(api, { sourceId: 'inspect-raw-id', logicalSourceKey: 'test:inspectable' });
|
||||
|
||||
@@ -516,7 +516,7 @@ test('inspect resolves a raw sourceId to its source via the stable logical key',
|
||||
|
||||
test('inspect by a shared logical key returns the native winner, not a suppressed duplicate', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
|
||||
// Register the compatibility duplicate FIRST so a naive logicalSourceKey-only match would pick it.
|
||||
await registerSource(api, { sourceId: 'compat-first', logicalSourceKey: 'dup:inspect', providerId: 'compat_p', compatibilitySource: 'legacy' });
|
||||
@@ -531,7 +531,7 @@ test('inspect by a shared logical key returns the native winner, not a suppresse
|
||||
|
||||
test('enumerate preserves a provider-supplied safeLabel through redaction', async () => {
|
||||
const window = loadAudioSession();
|
||||
const audioSession = window.slopsmith.audioSession;
|
||||
const audioSession = window.feedBack.audioSession;
|
||||
const provider = makeInputProvider({
|
||||
providerId: 'labelled',
|
||||
sourceId: 'labelled-bootstrap',
|
||||
@@ -551,7 +551,7 @@ test('enumerate preserves a provider-supplied safeLabel through redaction', asyn
|
||||
|
||||
test('enumerate returns no-handler when providers exist but none support source.enumerate', async () => {
|
||||
const window = loadAudioSession();
|
||||
const audioSession = window.slopsmith.audioSession;
|
||||
const audioSession = window.feedBack.audioSession;
|
||||
|
||||
audioSession.registerInputSource({ sourceId: 'nohandler-raw', logicalSourceKey: 'nh:key', providerId: 'nh_provider', operations: ['source.open'], operationHandlers: { 'source.open': () => ({ outcome: 'handled', status: 'open' }) } });
|
||||
|
||||
@@ -561,7 +561,7 @@ test('enumerate returns no-handler when providers exist but none support source.
|
||||
|
||||
test('open-source hint mismatch echoes a pseudonymized sourceId hint without leaking the raw id', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
|
||||
await registerSource(api, { sourceId: 'sel-raw', logicalSourceKey: 'test:sel' });
|
||||
await registerSource(api, { sourceId: 'other-raw', logicalSourceKey: 'test:other' });
|
||||
@@ -571,13 +571,13 @@ test('open-source hint mismatch echoes a pseudonymized sourceId hint without lea
|
||||
|
||||
assert.equal(open.outcome, 'degraded');
|
||||
assert.match(open.payload.sourceId, /^source-\d+$/);
|
||||
const encoded = JSON.stringify({ open, snapshot: window.slopsmith.audioSession.snapshot() });
|
||||
const encoded = JSON.stringify({ open, snapshot: window.feedBack.audioSession.snapshot() });
|
||||
assert.equal(encoded.includes('other-raw'), false);
|
||||
});
|
||||
|
||||
test('enumerate propagates a provider source.enumerate denial instead of empty success', async () => {
|
||||
const window = loadAudioSession();
|
||||
const audioSession = window.slopsmith.audioSession;
|
||||
const audioSession = window.feedBack.audioSession;
|
||||
const provider = makeInputProvider({
|
||||
providerId: 'denier',
|
||||
sourceId: 'denier-bootstrap',
|
||||
@@ -608,7 +608,7 @@ test('enumerate propagates a provider source.enumerate denial instead of empty s
|
||||
|
||||
test('enumerateInputSources returns distinct sourceId pseudonyms across sources', async () => {
|
||||
const window = loadAudioSession();
|
||||
const audioSession = window.slopsmith.audioSession;
|
||||
const audioSession = window.feedBack.audioSession;
|
||||
const provider = makeInputProvider({
|
||||
providerId: 'multi_provider',
|
||||
sourceId: 'mp-bootstrap',
|
||||
@@ -631,7 +631,7 @@ test('enumerateInputSources returns distinct sourceId pseudonyms across sources'
|
||||
|
||||
test('open-session ids correlate between openSessions and recentOutcomes in a snapshot', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
|
||||
await registerSource(api, { sourceId: 'corr-raw', logicalSourceKey: 'test:corr' });
|
||||
await api.dispatch({ capability: 'audio-input', command: 'select-source', source: 'user', payload: { logicalSourceKey: 'test:corr' } });
|
||||
@@ -639,7 +639,7 @@ test('open-session ids correlate between openSessions and recentOutcomes in a sn
|
||||
assert.equal(open.outcome, 'handled');
|
||||
|
||||
const openId = open.payload.openSessionId;
|
||||
const snap = window.slopsmith.audioSession.snapshot();
|
||||
const snap = window.feedBack.audioSession.snapshot();
|
||||
assert.equal(snap.domains['audio-input'].openSessions[0].openSessionId, openId);
|
||||
const openOutcome = snap.recentOutcomes.find(outcome => outcome.operation === 'open-source' && outcome.status === 'open' && outcome.openSessionId);
|
||||
assert.ok(openOutcome);
|
||||
@@ -649,8 +649,8 @@ test('open-session ids correlate between openSessions and recentOutcomes in a sn
|
||||
|
||||
test('startSession closes open input sessions from the previous session before replacing it', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const audioSession = window.slopsmith.audioSession;
|
||||
const api = window.feedBack.capabilities;
|
||||
const audioSession = window.feedBack.audioSession;
|
||||
let providerClosed = 0;
|
||||
|
||||
await registerSource(api, {
|
||||
@@ -677,8 +677,8 @@ test('a persisted selected-source key that is not redaction-safe is ignored on r
|
||||
const window = loadAudioSession();
|
||||
|
||||
// Simulate a tampered/mutated localStorage entry.
|
||||
window.localStorage.setItem('slopsmith.audioInput.selectedLogicalSourceKey', '/Users/me/evil token=zzz999');
|
||||
const snap = window.slopsmith.audioSession.startSession({ sessionId: 'main:restore-test' });
|
||||
window.localStorage.setItem('feedBack.audioInput.selectedLogicalSourceKey', '/Users/me/evil token=zzz999');
|
||||
const snap = window.feedBack.audioSession.startSession({ sessionId: 'main:restore-test' });
|
||||
|
||||
assert.equal(snap.domains['audio-input'].selected, null);
|
||||
const encoded = JSON.stringify(snap);
|
||||
@@ -688,8 +688,8 @@ test('a persisted selected-source key that is not redaction-safe is ignored on r
|
||||
|
||||
test('stopSession closes open input sessions and notifies providers', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const audioSession = window.slopsmith.audioSession;
|
||||
const api = window.feedBack.capabilities;
|
||||
const audioSession = window.feedBack.audioSession;
|
||||
const closedEvents = captureEvents(window, 'audio-input:source-closed');
|
||||
let providerClosed = 0;
|
||||
|
||||
@@ -716,17 +716,17 @@ test('stopSession closes open input sessions and notifies providers', async () =
|
||||
|
||||
test('startSession keeps the in-memory selection when persistence has failed, ignoring stale storage', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
|
||||
// Seed storage with a stale key, then make subsequent writes fail.
|
||||
window.localStorage.setItem('slopsmith.audioInput.selectedLogicalSourceKey', 'stale:old-input');
|
||||
window.localStorage.setItem('feedBack.audioInput.selectedLogicalSourceKey', 'stale:old-input');
|
||||
window.localStorage.setItem = () => { throw new Error('quota'); };
|
||||
|
||||
await registerSource(api, { sourceId: 'current-raw', logicalSourceKey: 'current:input' });
|
||||
await api.dispatch({ capability: 'audio-input', command: 'select-source', source: 'user', payload: { logicalSourceKey: 'current:input' } });
|
||||
assert.equal(window.slopsmith.audioSession.snapshot().domains['audio-input'].storageStatus, 'failed');
|
||||
assert.equal(window.feedBack.audioSession.snapshot().domains['audio-input'].storageStatus, 'failed');
|
||||
|
||||
const snap = window.slopsmith.audioSession.startSession({ sessionId: 'main:after-fail' });
|
||||
const snap = window.feedBack.audioSession.startSession({ sessionId: 'main:after-fail' });
|
||||
|
||||
// Must keep the in-memory 'current:input', not revert to the stale storage key.
|
||||
assert.equal(snap.domains['audio-input'].selected.logicalSourceKey, 'current:input');
|
||||
@@ -735,20 +735,20 @@ test('startSession keeps the in-memory selection when persistence has failed, ig
|
||||
|
||||
test('selected source persistence restore and storage-unavailable fallback are stable', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
|
||||
await registerSource(api, { sourceId: 'persisted-source', logicalSourceKey: 'persisted:input' });
|
||||
await api.dispatch({ capability: 'audio-input', command: 'select-source', source: 'user', payload: { logicalSourceKey: 'persisted:input' } });
|
||||
const afterStart = window.slopsmith.audioSession.startSession({ sessionId: 'main:next-song' });
|
||||
const afterStart = window.feedBack.audioSession.startSession({ sessionId: 'main:next-song' });
|
||||
assert.equal(afterStart.domains['audio-input'].selected.logicalSourceKey, 'persisted:input');
|
||||
assert.equal(afterStart.domains['audio-input'].selected.restoreStatus, 'restored');
|
||||
|
||||
const noStorageWindow = loadAudioSession();
|
||||
noStorageWindow.localStorage.setItem = () => { throw new Error('blocked'); };
|
||||
const noStorageApi = noStorageWindow.slopsmith.capabilities;
|
||||
const noStorageApi = noStorageWindow.feedBack.capabilities;
|
||||
await registerSource(noStorageApi, { sourceId: 'session-source', logicalSourceKey: 'session:input' });
|
||||
await noStorageApi.dispatch({ capability: 'audio-input', command: 'select-source', source: 'user', payload: { logicalSourceKey: 'session:input' } });
|
||||
const noStorageSnapshot = noStorageWindow.slopsmith.audioSession.startSession({ sessionId: 'main:no-storage-song' });
|
||||
const noStorageSnapshot = noStorageWindow.feedBack.audioSession.startSession({ sessionId: 'main:no-storage-song' });
|
||||
assert.equal(noStorageSnapshot.domains['audio-input'].storageStatus, 'failed');
|
||||
assert.equal(noStorageSnapshot.domains['audio-input'].selected.logicalSourceKey, 'session:input');
|
||||
});
|
||||
|
||||
@@ -4,9 +4,9 @@ const { loadAudioSession } = require('./audio_session_test_harness');
|
||||
|
||||
test('audio-mix commands inspect register and unregister participants', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
const events = [];
|
||||
window.slopsmith.on('audio-mix:participant-registered', event => events.push(event.detail.payload.participantId));
|
||||
window.feedBack.on('audio-mix:participant-registered', event => events.push(event.detail.payload.participantId));
|
||||
|
||||
const registered = await api.dispatch({
|
||||
capability: 'audio-mix',
|
||||
@@ -29,12 +29,12 @@ test('audio-mix commands inspect register and unregister participants', async ()
|
||||
assert.equal(inspected.payload.participants.some(p => p.participantId === 'plugin.delay'), true);
|
||||
assert.equal(events.includes('plugin.delay'), true);
|
||||
assert.equal(removed.status, 'applied');
|
||||
assert.equal(window.slopsmith.audioSession.snapshot().domains['audio-mix'].participants.some(p => p.participantId === 'plugin.delay'), false);
|
||||
assert.equal(window.feedBack.audioSession.snapshot().domains['audio-mix'].participants.some(p => p.participantId === 'plugin.delay'), false);
|
||||
});
|
||||
|
||||
test('audio-mix registration reports incompatible participants explicitly', async () => {
|
||||
const window = loadAudioSession();
|
||||
const result = await window.slopsmith.capabilities.dispatch({
|
||||
const result = await window.feedBack.capabilities.dispatch({
|
||||
capability: 'audio-mix',
|
||||
command: 'register-participant',
|
||||
source: 'test',
|
||||
@@ -43,13 +43,13 @@ test('audio-mix registration reports incompatible participants explicitly', asyn
|
||||
|
||||
assert.equal(result.status, 'incompatible-version');
|
||||
assert.equal(result.outcome, 'incompatible-version');
|
||||
assert.equal(window.slopsmith.audioSession.snapshot().recentOutcomes.at(-1).outcome, 'incompatible-version');
|
||||
assert.equal(window.feedBack.audioSession.snapshot().recentOutcomes.at(-1).outcome, 'incompatible-version');
|
||||
});
|
||||
|
||||
test('audio-mix lists required participant kinds and commits provider fader values', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const audioSession = window.slopsmith.audioSession;
|
||||
const api = window.feedBack.capabilities;
|
||||
const audioSession = window.feedBack.audioSession;
|
||||
audioSession.startSession({ sessionId: 'main:test-song', songKey: 'test-song', songFormat: 'sloppak' });
|
||||
|
||||
let pluginValue = 0.25;
|
||||
@@ -99,10 +99,10 @@ test('audio-mix lists required participant kinds and commits provider fader valu
|
||||
|
||||
test('audio-mix reports invalid unavailable and timed-out fader operations', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
const events = [];
|
||||
window.slopsmith.on('audio-mix:fader-unavailable', event => events.push(event.detail.payload.participantId));
|
||||
window.slopsmith.audioSession.startSession({ sessionId: 'main:test-song' });
|
||||
window.feedBack.on('audio-mix:fader-unavailable', event => events.push(event.detail.payload.participantId));
|
||||
window.feedBack.audioSession.startSession({ sessionId: 'main:test-song' });
|
||||
|
||||
await api.dispatch({
|
||||
capability: 'audio-mix',
|
||||
@@ -150,7 +150,7 @@ test('audio-mix reports invalid unavailable and timed-out fader operations', asy
|
||||
|
||||
test('audio-mix keeps pre-session participants pending then attaches them on session start', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
|
||||
await api.dispatch({
|
||||
capability: 'audio-mix',
|
||||
@@ -166,7 +166,7 @@ test('audio-mix keeps pre-session participants pending then attaches them on ses
|
||||
},
|
||||
});
|
||||
const pending = await api.dispatch({ capability: 'audio-mix', command: 'list-faders', source: 'test' });
|
||||
window.slopsmith.audioSession.startSession({ sessionId: 'main:next-song' });
|
||||
window.feedBack.audioSession.startSession({ sessionId: 'main:next-song' });
|
||||
const active = await api.dispatch({ capability: 'audio-mix', command: 'list-faders', source: 'test' });
|
||||
|
||||
assert.equal(pending.payload.faders.find(fader => fader.participantId === 'plugin.presession').availability, 'pending');
|
||||
@@ -175,7 +175,7 @@ test('audio-mix keeps pre-session participants pending then attaches them on ses
|
||||
|
||||
test('audio-mix registration is idempotent and song switching keeps known participants without stale route', async () => {
|
||||
const window = loadAudioSession();
|
||||
const audioSession = window.slopsmith.audioSession;
|
||||
const audioSession = window.feedBack.audioSession;
|
||||
audioSession.startSession({ sessionId: 'main:first-song', songKey: 'first-song' });
|
||||
audioSession.setRoute({ routeKind: 'stems', availability: 'available' });
|
||||
|
||||
@@ -189,11 +189,11 @@ test('audio-mix registration is idempotent and song switching keeps known partic
|
||||
operations: ['fader.get-value', 'fader.set-value'],
|
||||
});
|
||||
}
|
||||
const beforeStop = await window.slopsmith.capabilities.dispatch({ capability: 'audio-mix', command: 'list-faders', source: 'test' });
|
||||
const beforeStop = await window.feedBack.capabilities.dispatch({ capability: 'audio-mix', command: 'list-faders', source: 'test' });
|
||||
audioSession.stopSession('song switch');
|
||||
const stopped = audioSession.snapshot();
|
||||
audioSession.startSession({ sessionId: 'main:second-song', songKey: 'second-song' });
|
||||
const afterStart = await window.slopsmith.capabilities.dispatch({ capability: 'audio-mix', command: 'list-faders', source: 'test' });
|
||||
const afterStart = await window.feedBack.capabilities.dispatch({ capability: 'audio-mix', command: 'list-faders', source: 'test' });
|
||||
|
||||
assert.equal(beforeStop.payload.faders.filter(fader => fader.participantId === 'plugin.rehydrated').length, 1);
|
||||
assert.equal(stopped.domains['audio-mix'].route.availability, 'unavailable');
|
||||
@@ -207,7 +207,7 @@ test('re-registering a mix participant without handlers preserves the existing s
|
||||
// wiped the fader.set-value handler installed at init — the mixer slider
|
||||
// then moved visually but never applied the volume (archive and sloppak).
|
||||
const window = loadAudioSession();
|
||||
const audioSession = window.slopsmith.audioSession;
|
||||
const audioSession = window.feedBack.audioSession;
|
||||
audioSession.startSession({ sessionId: 'main:test-song', songKey: 'test-song', songFormat: 'sloppak' });
|
||||
|
||||
const applied = [];
|
||||
|
||||
@@ -23,7 +23,7 @@ async function installMonitoring(api, overrides = {}) {
|
||||
|
||||
test('audio-monitoring starts and stops through selected provider and source', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
const startedEvents = captureEvents(window, 'audio-monitoring:monitoring-started');
|
||||
const stoppedEvents = captureEvents(window, 'audio-monitoring:monitoring-stopped');
|
||||
const input = await installInput(api);
|
||||
@@ -31,7 +31,7 @@ test('audio-monitoring starts and stops through selected provider and source', a
|
||||
|
||||
const active = await api.dispatch({ capability: 'audio-monitoring', command: 'start', source: 'user', payload: { requesterId: 'user', authorization: 'user-action', requiredChannelShape: 'mono' } });
|
||||
const stopped = await api.dispatch({ capability: 'audio-monitoring', command: 'stop', source: 'user', payload: { requesterId: 'user', monitoringId: active.payload.monitoringId } });
|
||||
const snapshot = window.slopsmith.audioSession.snapshot().domains['audio-monitoring'];
|
||||
const snapshot = window.feedBack.audioSession.snapshot().domains['audio-monitoring'];
|
||||
|
||||
assert.equal(active.outcome, 'handled');
|
||||
assert.equal(active.payload.state, 'active');
|
||||
@@ -46,7 +46,7 @@ test('audio-monitoring starts and stops through selected provider and source', a
|
||||
|
||||
test('audio-monitoring redacts circular provider payloads without overflowing', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
const circular = { directMonitor: { state: 'muted' }, latencySummary: { bucket: 'low' } };
|
||||
circular.self = circular;
|
||||
await installInput(api);
|
||||
@@ -57,7 +57,7 @@ test('audio-monitoring redacts circular provider payloads without overflowing',
|
||||
});
|
||||
|
||||
const active = await api.dispatch({ capability: 'audio-monitoring', command: 'start', source: 'user', payload: { requesterId: 'user', authorization: 'user-action', requiredChannelShape: 'mono' } });
|
||||
const snapshot = window.slopsmith.audioSession.snapshot().domains['audio-monitoring'];
|
||||
const snapshot = window.feedBack.audioSession.snapshot().domains['audio-monitoring'];
|
||||
|
||||
assert.equal(active.outcome, 'handled');
|
||||
assert.equal(snapshot.sessions[0].state, 'active');
|
||||
@@ -65,12 +65,12 @@ test('audio-monitoring redacts circular provider payloads without overflowing',
|
||||
|
||||
test('audio-monitoring snapshots redact session identifiers and source refs', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
await installInput(api);
|
||||
await installMonitoring(api);
|
||||
|
||||
const active = await api.dispatch({ capability: 'audio-monitoring', command: 'start', source: 'user', payload: { requesterId: 'user', authorization: 'user-action', requiredChannelShape: 'mono' } });
|
||||
const snapshot = window.slopsmith.audioSession.snapshot().domains['audio-monitoring'];
|
||||
const snapshot = window.feedBack.audioSession.snapshot().domains['audio-monitoring'];
|
||||
const session = snapshot.sessions[0];
|
||||
const encoded = JSON.stringify(snapshot);
|
||||
|
||||
@@ -83,7 +83,7 @@ test('audio-monitoring snapshots redact session identifiers and source refs', as
|
||||
|
||||
test('audio-monitoring reports no provider unavailable degraded denied failed user action and reload boundaries', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
const degradedEvents = captureEvents(window, 'audio-monitoring:monitoring-degraded');
|
||||
const deniedEvents = captureEvents(window, 'audio-monitoring:monitoring-denied');
|
||||
const unavailableEvents = captureEvents(window, 'audio-monitoring:monitoring-unavailable');
|
||||
@@ -125,25 +125,25 @@ test('audio-monitoring reports no provider unavailable degraded denied failed us
|
||||
assert.equal(failedEvents.length >= 1, true);
|
||||
assert.equal(activeBeforeSwitch.outcome, 'handled');
|
||||
|
||||
window.slopsmith.audioSession.startSession({ sessionId: 'main:next-song' });
|
||||
const afterSongSwitch = window.slopsmith.audioSession.snapshot().domains['audio-monitoring'];
|
||||
window.feedBack.audioSession.startSession({ sessionId: 'main:next-song' });
|
||||
const afterSongSwitch = window.feedBack.audioSession.snapshot().domains['audio-monitoring'];
|
||||
assert.equal(afterSongSwitch.sessions.some(session => session.state === 'active'), true);
|
||||
|
||||
const restoredWindow = loadAudioSession({ storage: storageEntries(window) });
|
||||
const restored = restoredWindow.slopsmith.audioSession.snapshot().domains['audio-monitoring'];
|
||||
const restored = restoredWindow.feedBack.audioSession.snapshot().domains['audio-monitoring'];
|
||||
assert.equal(restored.sessions.length, 0);
|
||||
});
|
||||
|
||||
test('audio-monitoring provider registration is idempotent and selected provider is deterministic', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
await installInput(api);
|
||||
const legacy = await installMonitoring(api, { providerId: 'legacy_monitor', logicalMonitoringKey: 'shared:monitor', sourceMode: 'compatibility', compatibilitySource: 'legacy.monitor' });
|
||||
const native = await installMonitoring(api, { providerId: 'native_monitor', logicalMonitoringKey: 'shared:monitor', sourceMode: 'native', safeLabel: 'Native Monitor' });
|
||||
await api.dispatch({ capability: 'audio-monitoring', command: 'register-provider', source: 'native_monitor', payload: { ...native.provider, availability: 'pending' } });
|
||||
|
||||
const listed = await api.dispatch({ capability: 'audio-monitoring', command: 'list-providers', source: 'test' });
|
||||
const snapshot = window.slopsmith.audioSession.snapshot().domains['audio-monitoring'];
|
||||
const snapshot = window.feedBack.audioSession.snapshot().domains['audio-monitoring'];
|
||||
assert.equal(listed.payload.providers.length, 1);
|
||||
assert.equal(listed.payload.providers[0].providerId, 'native_monitor');
|
||||
assert.equal(snapshot.providers.some(provider => provider.providerId === 'legacy_monitor' && provider.supersededBy), true);
|
||||
@@ -160,7 +160,7 @@ test('audio-monitoring provider registration is idempotent and selected provider
|
||||
|
||||
test('audio-monitoring shares compatible sessions and stops provider after final requester', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
await installInput(api);
|
||||
const monitoring = await installMonitoring(api);
|
||||
|
||||
@@ -179,14 +179,14 @@ test('audio-monitoring shares compatible sessions and stops provider after final
|
||||
|
||||
const activeAgain = await api.dispatch({ capability: 'audio-monitoring', command: 'start', source: 'user', payload: { requesterId: 'user', authorization: 'user-action', requiredChannelShape: 'mono' } });
|
||||
await api.dispatch({ capability: 'audio-monitoring', command: 'unregister-provider', source: 'system', payload: { providerId: monitoring.provider.providerId } });
|
||||
const afterDisappear = window.slopsmith.audioSession.snapshot().domains['audio-monitoring'];
|
||||
const afterDisappear = window.feedBack.audioSession.snapshot().domains['audio-monitoring'];
|
||||
assert.equal(activeAgain.outcome, 'handled');
|
||||
assert.equal(afterDisappear.sessions.some(session => session.state === 'orphaned'), true);
|
||||
});
|
||||
|
||||
test('audio-monitoring owner can retry a stop after a transient provider stop failure', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
await installInput(api);
|
||||
let stopCalls = 0;
|
||||
await installMonitoring(api, {
|
||||
@@ -213,7 +213,7 @@ test('audio-monitoring owner can retry a stop after a transient provider stop fa
|
||||
|
||||
test('audio-monitoring surfaces a provider denial reason even when a direct-monitor conflict is present', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
await installInput(api);
|
||||
await installMonitoring(api, { startResult: { outcome: 'handled', status: 'denied', reason: 'microphone permission blocked' } });
|
||||
|
||||
@@ -229,11 +229,11 @@ test('audio-monitoring surfaces a provider denial reason even when a direct-moni
|
||||
|
||||
test('audio-monitoring normalizes an unsafe provider id before storing it', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
await installInput(api);
|
||||
|
||||
await api.dispatch({ capability: 'audio-monitoring', command: 'register-provider', source: 'evil', payload: { providerId: '/Users/secret token=abcdef0123456789 monitor', logicalMonitoringKey: 'evil:main', operations: ['monitoring.start'], operationHandlers: { 'monitoring.start': () => ({ outcome: 'handled', status: 'active' }) } } });
|
||||
const snapshot = window.slopsmith.audioSession.snapshot().domains['audio-monitoring'];
|
||||
const snapshot = window.feedBack.audioSession.snapshot().domains['audio-monitoring'];
|
||||
const provider = snapshot.providers[0];
|
||||
|
||||
// The surfaced providerId must be redacted + charset-restricted, never the raw path/token.
|
||||
@@ -244,7 +244,7 @@ test('audio-monitoring normalizes an unsafe provider id before storing it', asyn
|
||||
|
||||
test('audio-monitoring bounds caller-supplied identifiers reflected back in error reasons', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
await installInput(api);
|
||||
const evil = '/Users/secret/private token=supersecretvalue123 ' + 'x'.repeat(400);
|
||||
|
||||
@@ -266,7 +266,7 @@ test('audio-monitoring bounds caller-supplied identifiers reflected back in erro
|
||||
|
||||
test('audio-monitoring attaching to a shared session honors a conflicting direct-monitor requirement', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
await installInput(api);
|
||||
await installMonitoring(api);
|
||||
|
||||
@@ -284,7 +284,7 @@ test('audio-monitoring attaching to a shared session honors a conflicting direct
|
||||
|
||||
test('audio-monitoring start never lets a caller inject raw sourceRef fields', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
await installInput(api);
|
||||
await installMonitoring(api);
|
||||
|
||||
@@ -302,7 +302,7 @@ test('audio-monitoring start never lets a caller inject raw sourceRef fields', a
|
||||
|
||||
test('audio-monitoring start treats a void provider result as failed', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
await installInput(api);
|
||||
await installMonitoring(api, { providerId: 'void_monitor', logicalMonitoringKey: 'void:main', operationHandlers: { 'monitoring.start': () => undefined } });
|
||||
|
||||
@@ -314,14 +314,14 @@ test('audio-monitoring start treats a void provider result as failed', async ()
|
||||
|
||||
test('audio-monitoring stopAll requires explicit user action', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
await installInput(api);
|
||||
await installMonitoring(api);
|
||||
|
||||
const active = await api.dispatch({ capability: 'audio-monitoring', command: 'start', source: 'user', payload: { authorization: 'user-action', requiredChannelShape: 'mono' } });
|
||||
// A background requester must not be able to tear down everyone's monitoring.
|
||||
const background = await api.dispatch({ capability: 'audio-monitoring', command: 'stop', source: 'note_detect', payload: { stopAll: true } });
|
||||
const mid = window.slopsmith.audioSession.snapshot().domains['audio-monitoring'];
|
||||
const mid = window.feedBack.audioSession.snapshot().domains['audio-monitoring'];
|
||||
// An explicit user action can.
|
||||
const user = await api.dispatch({ capability: 'audio-monitoring', command: 'stop', source: 'user', payload: { stopAll: true, authorization: 'user-action' } });
|
||||
|
||||
@@ -333,7 +333,7 @@ test('audio-monitoring stopAll requires explicit user action', async () => {
|
||||
|
||||
test('audio-monitoring stop does not report stopped when the provider reports a terminal status', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
await installInput(api);
|
||||
await installMonitoring(api, { stopResult: { outcome: 'handled', status: 'failed', reason: 'device fell off the bus' } });
|
||||
|
||||
@@ -348,7 +348,7 @@ test('audio-monitoring stop does not report stopped when the provider reports a
|
||||
|
||||
test('audio-monitoring stop reports no-owner when the provider has disappeared', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
await installInput(api);
|
||||
const monitoring = await installMonitoring(api);
|
||||
|
||||
@@ -364,7 +364,7 @@ test('audio-monitoring stop reports no-owner when the provider has disappeared',
|
||||
|
||||
test('audio-monitoring stop reports unsupported-command when the provider has no stop operation', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
await installInput(api);
|
||||
await installMonitoring(api, { providerId: 'nostop_monitor', logicalMonitoringKey: 'nostop:main', operations: ['monitoring.start'] });
|
||||
|
||||
@@ -378,7 +378,7 @@ test('audio-monitoring stop reports unsupported-command when the provider has no
|
||||
|
||||
test('audio-monitoring events emit redaction-safe sessions', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
const startedEvents = captureEvents(window, 'audio-monitoring:monitoring-started');
|
||||
await installInput(api);
|
||||
await installMonitoring(api);
|
||||
@@ -397,7 +397,7 @@ test('audio-monitoring events emit redaction-safe sessions', async () => {
|
||||
|
||||
test('audio-monitoring rejects a providerId collision from a different owner', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
await installInput(api);
|
||||
const first = makeMonitoringProvider({ providerId: 'shared_id', ownerPluginId: 'plugin_a', logicalMonitoringKey: 'a:main' });
|
||||
const second = makeMonitoringProvider({ providerId: 'shared_id', ownerPluginId: 'plugin_b', logicalMonitoringKey: 'b:main' });
|
||||
@@ -408,7 +408,7 @@ test('audio-monitoring rejects a providerId collision from a different owner', a
|
||||
const sneaky = makeMonitoringProvider({ providerId: 'shared_id', logicalMonitoringKey: 'c:main' });
|
||||
delete sneaky.provider.ownerPluginId;
|
||||
const reg3 = await api.dispatch({ capability: 'audio-monitoring', command: 'register-provider', source: 'plugin_c', payload: sneaky.provider });
|
||||
const snapshot = window.slopsmith.audioSession.snapshot().domains['audio-monitoring'];
|
||||
const snapshot = window.feedBack.audioSession.snapshot().domains['audio-monitoring'];
|
||||
|
||||
assert.equal(reg1.outcome, 'handled');
|
||||
assert.equal(reg2.outcome, 'failed');
|
||||
@@ -420,12 +420,12 @@ test('audio-monitoring rejects a providerId collision from a different owner', a
|
||||
|
||||
test('audio-monitoring session keeps openInputSessionId verbatim for cross-domain correlation', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
await installInput(api);
|
||||
await installMonitoring(api);
|
||||
|
||||
const active = await api.dispatch({ capability: 'audio-monitoring', command: 'start', source: 'user', payload: { authorization: 'user-action', requiredChannelShape: 'mono' } });
|
||||
const session = window.slopsmith.audioSession.snapshot().domains['audio-monitoring'].sessions.at(-1);
|
||||
const session = window.feedBack.audioSession.snapshot().domains['audio-monitoring'].sessions.at(-1);
|
||||
|
||||
assert.equal(active.outcome, 'handled');
|
||||
assert.match(active.payload.openInputSessionId, /^input-open-\d+$/);
|
||||
@@ -437,12 +437,12 @@ test('audio-monitoring session keeps openInputSessionId verbatim for cross-domai
|
||||
|
||||
test('audio-monitoring status refresh tolerates a void provider result without faking active', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
await installInput(api);
|
||||
await installMonitoring(api, { availability: 'available', operationHandlers: { 'monitoring.status': () => undefined } });
|
||||
|
||||
await api.dispatch({ capability: 'audio-monitoring', command: 'inspect', source: 'user', payload: { includeStatus: true } });
|
||||
const outcomes = window.slopsmith.audioSession.snapshot().recentOutcomes.filter(entry => entry.domain === 'audio-monitoring' && entry.operation === 'status');
|
||||
const outcomes = window.feedBack.audioSession.snapshot().recentOutcomes.filter(entry => entry.domain === 'audio-monitoring' && entry.operation === 'status');
|
||||
|
||||
// A void status reply is tolerated but must not be recorded as an 'active' session.
|
||||
assert.equal(outcomes.length >= 1, true);
|
||||
@@ -451,7 +451,7 @@ test('audio-monitoring status refresh tolerates a void provider result without f
|
||||
|
||||
test('audio-monitoring status refresh does not leak the raw device sourceId to providers', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
await installInput(api);
|
||||
let statusSourceRef = null;
|
||||
await installMonitoring(api, { operationHandlers: { 'monitoring.status': (request) => { statusSourceRef = request.sourceRef; return { outcome: 'handled', status: 'active' }; } } });
|
||||
@@ -466,7 +466,7 @@ test('audio-monitoring status refresh does not leak the raw device sourceId to p
|
||||
|
||||
test('audio-monitoring status refresh does not downgrade availability on a non-state provider reply', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
await installInput(api);
|
||||
await installMonitoring(api, { availability: 'available', statusResult: { outcome: 'no-handler' } });
|
||||
|
||||
@@ -480,7 +480,7 @@ test('audio-monitoring status refresh does not downgrade availability on a non-s
|
||||
|
||||
test('audio-monitoring re-keys active sessions on a preference change so requesters still attach', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
await installInput(api);
|
||||
await installMonitoring(api);
|
||||
|
||||
@@ -499,13 +499,13 @@ test('audio-monitoring re-keys active sessions on a preference change so request
|
||||
|
||||
test('audio-monitoring does not assume direct-monitor applied without provider confirmation', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
await installInput(api);
|
||||
await installMonitoring(api, { directMonitorResult: { outcome: 'handled' } });
|
||||
|
||||
await api.dispatch({ capability: 'audio-monitoring', command: 'start', source: 'user', payload: { authorization: 'user-action', requiredChannelShape: 'mono' } });
|
||||
const changed = await api.dispatch({ capability: 'audio-monitoring', command: 'set-direct-monitor', source: 'user', payload: { state: 'unmuted' } });
|
||||
const snapshot = window.slopsmith.audioSession.snapshot().domains['audio-monitoring'];
|
||||
const snapshot = window.feedBack.audioSession.snapshot().domains['audio-monitoring'];
|
||||
|
||||
assert.equal(changed.outcome, 'handled');
|
||||
// The provider handled the request but did not confirm application, so applied must stay false.
|
||||
@@ -515,7 +515,7 @@ test('audio-monitoring does not assume direct-monitor applied without provider c
|
||||
|
||||
test('audio-monitoring direct-monitor summary preserves an unavailable control state', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
await installInput(api);
|
||||
await installMonitoring(api, {
|
||||
directMonitorResult: { outcome: 'handled', summary: { directMonitor: { state: 'unmuted', control: 'unavailable', applied: false, reason: 'temporarily unavailable' } } },
|
||||
@@ -523,7 +523,7 @@ test('audio-monitoring direct-monitor summary preserves an unavailable control s
|
||||
|
||||
await api.dispatch({ capability: 'audio-monitoring', command: 'start', source: 'user', payload: { authorization: 'user-action', requiredChannelShape: 'mono' } });
|
||||
const changed = await api.dispatch({ capability: 'audio-monitoring', command: 'set-direct-monitor', source: 'user', payload: { state: 'unmuted' } });
|
||||
const snapshot = window.slopsmith.audioSession.snapshot().domains['audio-monitoring'];
|
||||
const snapshot = window.feedBack.audioSession.snapshot().domains['audio-monitoring'];
|
||||
|
||||
// A session that reports control 'unavailable' must not be collapsed to 'unknown' in the rollup.
|
||||
assert.equal(changed.payload.directMonitor.control, 'unavailable');
|
||||
@@ -533,7 +533,7 @@ test('audio-monitoring direct-monitor summary preserves an unavailable control s
|
||||
|
||||
test('audio-monitoring direct-monitor summary reflects a provider that handles but does not apply', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
await installInput(api);
|
||||
await installMonitoring(api, {
|
||||
directMonitorResult: { outcome: 'handled', summary: { directMonitor: { state: 'unmuted', control: 'supported', applied: false, reason: 'hardware busy' } } },
|
||||
@@ -541,7 +541,7 @@ test('audio-monitoring direct-monitor summary reflects a provider that handles b
|
||||
|
||||
await api.dispatch({ capability: 'audio-monitoring', command: 'start', source: 'user', payload: { authorization: 'user-action', requiredChannelShape: 'mono' } });
|
||||
const changed = await api.dispatch({ capability: 'audio-monitoring', command: 'set-direct-monitor', source: 'user', payload: { state: 'unmuted' } });
|
||||
const snapshot = window.slopsmith.audioSession.snapshot().domains['audio-monitoring'];
|
||||
const snapshot = window.feedBack.audioSession.snapshot().domains['audio-monitoring'];
|
||||
|
||||
assert.equal(changed.outcome, 'handled');
|
||||
// Provider handled the request but reported it was not applied; the domain summary must not
|
||||
@@ -556,13 +556,13 @@ test('audio-monitoring direct-monitor summary reflects a provider that handles b
|
||||
|
||||
test('audio-monitoring direct monitor preference is user authoritative', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
await installInput(api);
|
||||
const monitoring = await installMonitoring(api, { directMonitor: { state: 'muted', control: 'supported', preference: 'muted', applied: true } });
|
||||
|
||||
const changed = await api.dispatch({ capability: 'audio-monitoring', command: 'set-direct-monitor', source: 'user', payload: { state: 'unmuted' } });
|
||||
const startConflict = await api.dispatch({ capability: 'audio-monitoring', command: 'start', source: 'note_detect', payload: { requesterId: 'note_detect', authorization: 'user-action', directMonitorRequirement: 'muted', requiredChannelShape: 'mono' } });
|
||||
const snapshot = window.slopsmith.audioSession.snapshot().domains['audio-monitoring'];
|
||||
const snapshot = window.feedBack.audioSession.snapshot().domains['audio-monitoring'];
|
||||
|
||||
assert.equal(changed.outcome, 'handled');
|
||||
assert.equal(startConflict.outcome, 'degraded');
|
||||
@@ -574,14 +574,14 @@ test('audio-monitoring direct monitor preference is user authoritative', async (
|
||||
|
||||
test('audio-monitoring direct monitor unsupported control remains diagnosable', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
const changedEvents = captureEvents(window, 'audio-monitoring:direct-monitor-changed');
|
||||
await installInput(api);
|
||||
await installMonitoring(api, { providerId: 'noctl_monitor', logicalMonitoringKey: 'noctl:monitor', operations: ['monitoring.start', 'monitoring.stop'] });
|
||||
|
||||
const active = await api.dispatch({ capability: 'audio-monitoring', command: 'start', source: 'user', payload: { providerId: 'noctl_monitor', requesterId: 'user', authorization: 'user-action', requiredChannelShape: 'mono' } });
|
||||
const changed = await api.dispatch({ capability: 'audio-monitoring', command: 'set-direct-monitor', source: 'user', payload: { state: 'unmuted' } });
|
||||
const snapshot = window.slopsmith.audioSession.snapshot().domains['audio-monitoring'];
|
||||
const snapshot = window.feedBack.audioSession.snapshot().domains['audio-monitoring'];
|
||||
|
||||
assert.equal(active.outcome, 'handled');
|
||||
assert.equal(changed.outcome, 'unsupported-command');
|
||||
@@ -592,7 +592,7 @@ test('audio-monitoring direct monitor unsupported control remains diagnosable',
|
||||
|
||||
test('audio-monitoring distinguishes failure outcomes and prompt-free status inspection', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
const input = await installInput(api, { channelSummary: { channelCount: 1, channelShape: 'mono', supports: ['mono'] } });
|
||||
const monitoring = await installMonitoring(api);
|
||||
|
||||
@@ -621,7 +621,7 @@ test('audio-monitoring distinguishes failure outcomes and prompt-free status ins
|
||||
assert.equal(monitoring.calls.some(call => call[0] === 'monitoring.status'), true);
|
||||
assert.equal(monitoring.calls.some(call => call[0] === 'monitoring.start'), false);
|
||||
|
||||
const outcomes = window.slopsmith.audioSession.snapshot().recentOutcomes.filter(entry => entry.domain === 'audio-monitoring');
|
||||
const outcomes = window.feedBack.audioSession.snapshot().recentOutcomes.filter(entry => entry.domain === 'audio-monitoring');
|
||||
for (const outcome of ['handled', 'unsupported-command', 'incompatible', 'incompatible-version', 'no-handler']) {
|
||||
assert.equal(outcomes.some(entry => entry.outcome === outcome), true, `missing outcome ${outcome}`);
|
||||
}
|
||||
@@ -629,12 +629,12 @@ test('audio-monitoring distinguishes failure outcomes and prompt-free status ins
|
||||
|
||||
test('audio-session diagnostics remain bounded during frequent input monitoring updates', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
for (let index = 0; index < 80; index += 1) {
|
||||
await api.dispatch({ capability: 'audio-input', command: 'register-source', source: 'bench', payload: { sourceId: `source-${index}`, logicalSourceKey: `bench:source:${index}`, providerId: 'bench', safeLabel: `/Users/example/private-${index}`, channelSummary: { channelCount: 1, channelShape: 'mono', supports: ['mono'] }, operations: ['source.open'], operationHandlers: { 'source.open': () => ({ outcome: 'handled' }) } } });
|
||||
await api.dispatch({ capability: 'audio-monitoring', command: 'register-provider', source: 'bench', payload: { providerId: `monitor-${index}`, logicalMonitoringKey: `bench:monitor:${index}`, operations: ['monitoring.start'], operationHandlers: { 'monitoring.start': () => ({ outcome: 'handled', status: 'active' }) } } });
|
||||
}
|
||||
const snapshot = window.slopsmith.audioSession.snapshot();
|
||||
const snapshot = window.feedBack.audioSession.snapshot();
|
||||
const encoded = JSON.stringify(snapshot);
|
||||
assert.equal(snapshot.recentOutcomes.length <= 100, true);
|
||||
assert.equal(encoded.length < 96 * 1024, true);
|
||||
|
||||
@@ -4,7 +4,7 @@ const { loadAudioSession, runBrowserScript, installMixerDom } = require('./audio
|
||||
|
||||
test('audio session records route transitions without blocking callers', () => {
|
||||
const window = loadAudioSession();
|
||||
const audioSession = window.slopsmith.audioSession;
|
||||
const audioSession = window.feedBack.audioSession;
|
||||
|
||||
const html5 = audioSession.setRoute({ routeKind: 'html5', availability: 'available', selectedByUser: true });
|
||||
const stems = audioSession.setRoute({ routeKind: 'stems', availability: 'available', selectedByUser: true });
|
||||
@@ -24,10 +24,10 @@ test('legacy song fader registration is bridged into audio-mix participants and
|
||||
window.localStorage.setItem('volume', '65');
|
||||
|
||||
runBrowserScript(window, 'static/audio-mixer.js');
|
||||
assert.equal(typeof window.slopsmith.audio.applySongVolume, 'function');
|
||||
assert.equal(typeof window.feedBack.audio.applySongVolume, 'function');
|
||||
|
||||
await window.slopsmith.audio.applySongVolume(72);
|
||||
const snapshot = window.slopsmith.audioSession.snapshot();
|
||||
await window.feedBack.audio.applySongVolume(72);
|
||||
const snapshot = window.feedBack.audioSession.snapshot();
|
||||
const songParticipant = snapshot.domains['audio-mix'].participants.find(p => p.participantId === 'core.song');
|
||||
|
||||
assert.equal(audio.volume, 0.72);
|
||||
@@ -43,27 +43,27 @@ test('song volume persists through html5 stems and desktop routes', async () =>
|
||||
const stemsCalls = [];
|
||||
const desktopCalls = [];
|
||||
window.localStorage.setItem('volume', '41');
|
||||
window.slopsmith.stems = { setMasterVolume(value) { stemsCalls.push(value); return Promise.resolve(); } };
|
||||
window.slopsmithDesktop = { audio: { setGain(name, value) { desktopCalls.push([name, value]); return Promise.resolve(); } } };
|
||||
window.feedBack.stems = { setMasterVolume(value) { stemsCalls.push(value); return Promise.resolve(); } };
|
||||
window.feedBackDesktop = { audio: { setGain(name, value) { desktopCalls.push([name, value]); return Promise.resolve(); } } };
|
||||
|
||||
runBrowserScript(window, 'static/audio-mixer.js');
|
||||
assert.equal(window.slopsmith.audio.readSongVolume(), 41);
|
||||
assert.equal(window.feedBack.audio.readSongVolume(), 41);
|
||||
|
||||
await window.slopsmith.audio.applySongVolume(55);
|
||||
await window.feedBack.audio.applySongVolume(55);
|
||||
assert.equal(audio.volume, 0.55);
|
||||
assert.equal(stemsCalls.at(-1), 0.55);
|
||||
assert.equal(window.slopsmith.audioSession.snapshot().domains['audio-mix'].route.routeKind, 'stems');
|
||||
assert.equal(window.feedBack.audioSession.snapshot().domains['audio-mix'].route.routeKind, 'stems');
|
||||
|
||||
window._juceMode = true;
|
||||
delete window.slopsmith.stems;
|
||||
await window.slopsmith.audio.applySongVolume(66);
|
||||
delete window.feedBack.stems;
|
||||
await window.feedBack.audio.applySongVolume(66);
|
||||
assert.deepEqual(desktopCalls.at(-1), ['backing', 0.66]);
|
||||
assert.equal(window.slopsmith.audioSession.snapshot().domains['audio-mix'].route.routeKind, 'juce');
|
||||
assert.equal(window.feedBack.audioSession.snapshot().domains['audio-mix'].route.routeKind, 'juce');
|
||||
});
|
||||
|
||||
test('stems provider ownership remains separate from audio-mix stem participation', async () => {
|
||||
const window = loadAudioSession();
|
||||
const audioSession = window.slopsmith.audioSession;
|
||||
const audioSession = window.feedBack.audioSession;
|
||||
audioSession.startSession({ sessionId: 'main:stems-song' });
|
||||
audioSession.registerStemOwner({ ownerId: 'stems_plugin', stemIds: ['guitar', 'bass'], availability: 'available' });
|
||||
audioSession.registerMixParticipant({
|
||||
@@ -76,8 +76,8 @@ test('stems provider ownership remains separate from audio-mix stem participatio
|
||||
operations: ['fader.get-value', 'fader.set-value'],
|
||||
});
|
||||
|
||||
const stemsInspect = await window.slopsmith.capabilities.dispatch({ capability: 'stems', command: 'inspect', source: 'test' });
|
||||
const mixInspect = await window.slopsmith.capabilities.dispatch({ capability: 'audio-mix', command: 'inspect', source: 'test' });
|
||||
const stemsInspect = await window.feedBack.capabilities.dispatch({ capability: 'stems', command: 'inspect', source: 'test' });
|
||||
const mixInspect = await window.feedBack.capabilities.dispatch({ capability: 'audio-mix', command: 'inspect', source: 'test' });
|
||||
|
||||
assert.equal(stemsInspect.payload.owner.ownerId, 'stems_plugin');
|
||||
assert.equal(mixInspect.payload.faders.some(fader => fader.kind === 'stem' && fader.ownerPluginId === 'stems_plugin'), true);
|
||||
@@ -85,8 +85,8 @@ test('stems provider ownership remains separate from audio-mix stem participatio
|
||||
|
||||
test('audio-input selection and registered providers survive song session switches without live sessions', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const audioSession = window.slopsmith.audioSession;
|
||||
const api = window.feedBack.capabilities;
|
||||
const audioSession = window.feedBack.audioSession;
|
||||
|
||||
audioSession.startSession({ sessionId: 'main:first-song', songKey: 'first-song.sloppak', songFormat: 'sloppak' });
|
||||
await api.dispatch({
|
||||
|
||||
@@ -4,8 +4,8 @@ const { loadAudioSession } = require('./audio_session_test_harness');
|
||||
|
||||
test('stem owner claim restore orphan and manual override lifecycle is recorded', async () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const audioSession = window.slopsmith.audioSession;
|
||||
const api = window.feedBack.capabilities;
|
||||
const audioSession = window.feedBack.audioSession;
|
||||
|
||||
const noOwner = await api.dispatch({ capability: 'stems', command: 'mute', source: 'nam_tone', payload: { stemIds: ['guitar'] } });
|
||||
assert.equal(noOwner.outcome, 'no-owner');
|
||||
@@ -26,8 +26,8 @@ test('stem owner claim restore orphan and manual override lifecycle is recorded'
|
||||
|
||||
test('audio session coordinates stems without replacing the active stems owner', () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const audioSession = window.slopsmith.audioSession;
|
||||
const api = window.feedBack.capabilities;
|
||||
const audioSession = window.feedBack.audioSession;
|
||||
|
||||
let stemsPipeline = api.inspect('stems');
|
||||
let coordinator = stemsPipeline.participants.find(entry => entry.pluginId === 'core.audio.session');
|
||||
@@ -46,8 +46,8 @@ test('audio session coordinates stems without replacing the active stems owner',
|
||||
|
||||
test('stem automation claims become orphaned when owner disappears', () => {
|
||||
const window = loadAudioSession();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const audioSession = window.slopsmith.audioSession;
|
||||
const api = window.feedBack.capabilities;
|
||||
const audioSession = window.feedBack.audioSession;
|
||||
const unavailableEvents = [];
|
||||
api.subscribe('stems:owner-unavailable', detail => unavailableEvents.push(detail));
|
||||
|
||||
|
||||
@@ -6,13 +6,13 @@ test('Stems master-volume compatibility bridge hit is attributed through audio s
|
||||
const window = loadAudioSession();
|
||||
const calls = [];
|
||||
installMixerDom(window);
|
||||
window.slopsmith.stems = { setMasterVolume(value) { calls.push(value); } };
|
||||
window.feedBack.stems = { setMasterVolume(value) { calls.push(value); } };
|
||||
|
||||
runBrowserScript(window, 'static/audio-mixer.js');
|
||||
await window.slopsmith.audio.applySongVolume(50);
|
||||
await window.feedBack.audio.applySongVolume(50);
|
||||
|
||||
const snapshot = window.slopsmith.audioSession.snapshot();
|
||||
const snapshot = window.feedBack.audioSession.snapshot();
|
||||
assert.deepEqual(calls, [0.5]);
|
||||
assert.equal(snapshot.domains.stems.bridges.some(bridge => bridge.bridgeId === 'stems.master-volume'), true);
|
||||
assert.equal(window.slopsmith.capabilities.snapshotDiagnostics().compatibilityShims.some(shim => shim.shimId === 'stems.master-volume' && shim.hitCount >= 1), true);
|
||||
assert.equal(window.feedBack.capabilities.snapshotDiagnostics().compatibilityShims.some(shim => shim.shimId === 'stems.master-volume' && shim.hitCount >= 1), true);
|
||||
});
|
||||
@@ -23,12 +23,12 @@ function runBrowserScript(window, relativePath) {
|
||||
|
||||
function captureEvents(window, eventName) {
|
||||
const events = [];
|
||||
window.slopsmith.on(eventName, event => events.push(event.detail));
|
||||
window.feedBack.on(eventName, event => events.push(event.detail));
|
||||
return events;
|
||||
}
|
||||
|
||||
function diagnosticsSnapshot(window) {
|
||||
return window.slopsmith.audioSession.snapshot();
|
||||
return window.feedBack.audioSession.snapshot();
|
||||
}
|
||||
|
||||
function storageEntries(window) {
|
||||
@@ -175,7 +175,7 @@ function installMixerDom(window) {
|
||||
|
||||
function loadAudioMixer(window) {
|
||||
runBrowserScript(window, path.relative(ROOT, AUDIO_MIXER_JS));
|
||||
return window.slopsmith.audio;
|
||||
return window.feedBack.audio;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -51,7 +51,7 @@ function runOverlay(nodes) {
|
||||
function runResolve({ override = null, screens = [], active = null } = {}) {
|
||||
const fnSrc = extractFunction(SRC, 'function _resolvePlayerOrigin(');
|
||||
const sandbox = {
|
||||
window: { slopsmith: { _nextReturnScreen: override } },
|
||||
window: { feedBack: { _nextReturnScreen: override } },
|
||||
document: {
|
||||
getElementById: (id) => (screens.includes(id) ? { id } : null),
|
||||
querySelector: () => (active ? { id: active } : null),
|
||||
@@ -59,16 +59,16 @@ function runResolve({ override = null, screens = [], active = null } = {}) {
|
||||
};
|
||||
vm.createContext(sandbox);
|
||||
vm.runInContext(fnSrc + '\nglobalThis.__r = _resolvePlayerOrigin();', sandbox);
|
||||
return { result: sandbox.__r, override: sandbox.window.slopsmith._nextReturnScreen };
|
||||
return { result: sandbox.__r, override: sandbox.window.feedBack._nextReturnScreen };
|
||||
}
|
||||
|
||||
// holdAutoExit() + _clearAutoExit() share module state; assemble them in one
|
||||
// sandbox to exercise the generation guard on the returned release handle.
|
||||
function buildHoldSandbox() {
|
||||
const clearSrc = extractFunction(SRC, 'function _clearAutoExit(');
|
||||
const holdSrc = extractFunction(SRC, 'window.slopsmith.holdAutoExit = function ()');
|
||||
const holdSrc = extractFunction(SRC, 'window.feedBack.holdAutoExit = function ()');
|
||||
const sandbox = { __closeCount: 0 };
|
||||
sandbox.window = { slopsmith: {}, closeCurrentSong: () => { sandbox.__closeCount++; } };
|
||||
sandbox.window = { feedBack: {}, closeCurrentSong: () => { sandbox.__closeCount++; } };
|
||||
vm.createContext(sandbox);
|
||||
vm.runInContext(`
|
||||
var _autoExitTimer = null;
|
||||
@@ -78,7 +78,7 @@ function buildHoldSandbox() {
|
||||
${clearSrc}
|
||||
${holdSrc}
|
||||
globalThis.__clearAutoExit = _clearAutoExit;
|
||||
globalThis.__hold = window.slopsmith.holdAutoExit;
|
||||
globalThis.__hold = window.feedBack.holdAutoExit;
|
||||
`, sandbox);
|
||||
return sandbox;
|
||||
}
|
||||
|
||||
@@ -31,13 +31,13 @@ function getCaseBlock(src, label) {
|
||||
|
||||
test('beats:loaded emit is wired into the WS beats case', () => {
|
||||
// Source-level guard: catch a future contributor removing the emit
|
||||
// (regression) or replacing window.slopsmith.emit with something
|
||||
// (regression) or replacing window.feedBack.emit with something
|
||||
// else (intentional refactor — this test then needs updating).
|
||||
const src = fs.readFileSync(HIGHWAY_JS, 'utf8');
|
||||
const block = getCaseBlock(src, 'beats');
|
||||
assert.match(
|
||||
block,
|
||||
/window\.slopsmith\.emit\(\s*['"]beats:loaded['"]/,
|
||||
/window\.feedBack\.emit\(\s*['"]beats:loaded['"]/,
|
||||
'beats case must emit beats:loaded',
|
||||
);
|
||||
assert.match(
|
||||
@@ -47,33 +47,33 @@ test('beats:loaded emit is wired into the WS beats case', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('beats:loaded emit is guarded against missing window.slopsmith', () => {
|
||||
// The WS handler can fire before the slopsmith namespace is defined
|
||||
test('beats:loaded emit is guarded against missing window.feedBack', () => {
|
||||
// The WS handler can fire before the feedBack namespace is defined
|
||||
// (early in app boot). The emit must be guarded so a missing
|
||||
// namespace doesn't throw inside the WS message dispatcher.
|
||||
// Looser pattern accepts any guard that reads window.slopsmith
|
||||
// Looser pattern accepts any guard that reads window.feedBack
|
||||
// (including typeof checks and combined conditions) rather than
|
||||
// mandating the exact `if (window.slopsmith)` form.
|
||||
// mandating the exact `if (window.feedBack)` form.
|
||||
const src = fs.readFileSync(HIGHWAY_JS, 'utf8');
|
||||
const block = getCaseBlock(src, 'beats');
|
||||
assert.match(
|
||||
block,
|
||||
/if\s*\(\s*[^)]*window\.slopsmith\b[^)]*\)/,
|
||||
'beats:loaded emit must be guarded against a missing window.slopsmith',
|
||||
/if\s*\(\s*[^)]*window\.feedBack\b[^)]*\)/,
|
||||
'beats:loaded emit must be guarded against a missing window.feedBack',
|
||||
);
|
||||
});
|
||||
|
||||
test('beats:loaded guard verifies emit is callable (typeof check)', () => {
|
||||
// A partially-attached namespace (window.slopsmith exists but emit
|
||||
// A partially-attached namespace (window.feedBack exists but emit
|
||||
// isn't a function yet during early boot) would throw without this
|
||||
// extra check. A truthy check (`window.slopsmith.emit && ...`) lets
|
||||
// extra check. A truthy check (`window.feedBack.emit && ...`) lets
|
||||
// non-callable values pass; require an explicit typeof === 'function'
|
||||
// check so the guard catches that real edge.
|
||||
const src = fs.readFileSync(HIGHWAY_JS, 'utf8');
|
||||
const block = getCaseBlock(src, 'beats');
|
||||
assert.match(
|
||||
block,
|
||||
/typeof\s+window\.slopsmith\.emit\s*===\s*['"]function['"]/,
|
||||
/typeof\s+window\.feedBack\.emit\s*===\s*['"]function['"]/,
|
||||
'guard must use typeof === \'function\' (not just truthy) to confirm emit is callable',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -24,7 +24,7 @@ function registerStemsOwner(api, calls) {
|
||||
|
||||
test('claim dispatch release records lifecycle and removes active claim', async () => {
|
||||
const window = loadCapabilities();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
const calls = [];
|
||||
registerStemsOwner(api, calls);
|
||||
|
||||
@@ -48,7 +48,7 @@ test('claim dispatch release records lifecycle and removes active claim', async
|
||||
|
||||
test('manual override is terminal for matching active claim target', async () => {
|
||||
const window = loadCapabilities();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
const calls = [];
|
||||
registerStemsOwner(api, calls);
|
||||
api.claim({ capability: 'stems', claimId: 'nam.amp-active', requester: 'nam_tone', target: { kind: 'guitar' } });
|
||||
|
||||
@@ -4,7 +4,7 @@ const { loadCapabilities } = require('./capabilities_test_harness');
|
||||
|
||||
test('diagnostics snapshots redact paths and trim recent decisions under 64 KB', async () => {
|
||||
const window = loadCapabilities();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
api.registerParticipant('owner', {
|
||||
stems: {
|
||||
roles: ['owner'],
|
||||
@@ -26,7 +26,7 @@ test('diagnostics snapshots redact paths and trim recent decisions under 64 KB',
|
||||
|
||||
test('compatibility shim hit counts and attribution are exported', () => {
|
||||
const window = loadCapabilities();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
api.registerCompatibilityShim({ shimId: 'stems:legacy-window', source: 'stems', capability: 'stems', legacySurface: 'window._stemsState', status: 'active', reason: 'legacy global bridge' });
|
||||
api.registerCompatibilityShim({ shimId: 'stems:legacy-window', source: 'stems', capability: 'stems', legacySurface: 'window._stemsState', status: 'used', used: true });
|
||||
api.registerCompatibilityShim({ shimId: 'stems:legacy-window', source: 'stems', capability: 'stems', legacySurface: 'window._stemsState', status: 'used', hit: true });
|
||||
@@ -39,7 +39,7 @@ test('compatibility shim hit counts and attribution are exported', () => {
|
||||
|
||||
test('diagnostics export expected compatibility shim surfaces', () => {
|
||||
const window = loadCapabilities();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
const expected = api.snapshotDiagnostics().expectedCompatibilityShims;
|
||||
|
||||
assert.ok(Array.isArray(expected));
|
||||
@@ -52,7 +52,7 @@ test('diagnostics export expected compatibility shim surfaces', () => {
|
||||
|
||||
test('diagnostics include active playback but exclude deferred and documentation-only future core domains', () => {
|
||||
const window = loadCapabilities();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
api.registerParticipant('playback_probe', {
|
||||
playback: {
|
||||
roles: ['provider'],
|
||||
@@ -95,7 +95,7 @@ test('diagnostics include active playback but exclude deferred and documentation
|
||||
|
||||
test('server-reported shim hit counts are not inflated by refresh registration', () => {
|
||||
const window = loadCapabilities();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
const shim = {
|
||||
shimId: 'stems:legacy-window',
|
||||
source: 'stems',
|
||||
@@ -114,7 +114,7 @@ test('server-reported shim hit counts are not inflated by refresh registration',
|
||||
|
||||
test('recordLegacyHit preserves non-library shim attribution', () => {
|
||||
const window = loadCapabilities();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
const first = api.recordLegacyHit({
|
||||
capability: 'stems',
|
||||
legacySurface: 'window._stemsState',
|
||||
@@ -136,7 +136,7 @@ test('recordLegacyHit preserves non-library shim attribution', () => {
|
||||
|
||||
test('recordLegacyHit counts runtime use and preserves used status across active refreshes', () => {
|
||||
const window = loadCapabilities();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
const first = api.recordLegacyHit({
|
||||
capability: 'stems',
|
||||
legacySurface: 'window._stemsState',
|
||||
@@ -168,9 +168,9 @@ test('recordLegacyHit counts runtime use and preserves used status across active
|
||||
|
||||
test('capability diagnostics emit a changed event after runtime updates', async () => {
|
||||
const window = loadCapabilities();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
let changes = 0;
|
||||
window.addEventListener('slopsmith:capabilities:changed', () => { changes += 1; });
|
||||
window.addEventListener('feedBack:capabilities:changed', () => { changes += 1; });
|
||||
|
||||
api.recordLegacyHit({
|
||||
capability: 'stems',
|
||||
|
||||
@@ -2,26 +2,26 @@ const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const { loadCapabilities } = require('./capabilities_test_harness');
|
||||
|
||||
test('capability runtime installs early slopsmith event bus', () => {
|
||||
test('capability runtime installs early feedBack event bus', () => {
|
||||
const window = loadCapabilities();
|
||||
const events = [];
|
||||
const onceEvents = [];
|
||||
|
||||
window.slopsmith.on('screen:changed', event => events.push(event.detail));
|
||||
window.slopsmith.on('song:ready', event => onceEvents.push(event.detail), { once: true });
|
||||
window.feedBack.on('screen:changed', event => events.push(event.detail));
|
||||
window.feedBack.on('song:ready', event => onceEvents.push(event.detail), { once: true });
|
||||
|
||||
window.slopsmith.emit('screen:changed', { id: 'home' });
|
||||
window.slopsmith.emit('song:ready', { title: 'First' });
|
||||
window.slopsmith.emit('song:ready', { title: 'Second' });
|
||||
window.feedBack.emit('screen:changed', { id: 'home' });
|
||||
window.feedBack.emit('song:ready', { title: 'First' });
|
||||
window.feedBack.emit('song:ready', { title: 'Second' });
|
||||
|
||||
assert.deepEqual(events, [{ id: 'home' }]);
|
||||
assert.deepEqual(onceEvents, [{ title: 'First' }]);
|
||||
assert.equal(typeof window.slopsmith.off, 'function');
|
||||
assert.equal(typeof window.feedBack.off, 'function');
|
||||
});
|
||||
|
||||
test('unregistering requester releases its claims', () => {
|
||||
const window = loadCapabilities();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
api.registerParticipant('stems', { stems: { roles: ['owner'], commands: ['mute'], handlers: { mute: () => ({ outcome: 'handled' }) }, runtime: true } });
|
||||
api.registerParticipant('nam_tone', { stems: { roles: ['requester'], commands: ['mute'], runtime: true } });
|
||||
api.claim({ capability: 'stems', claimId: 'nam.amp-active', requester: 'nam_tone' });
|
||||
@@ -34,7 +34,7 @@ test('unregistering requester releases its claims', () => {
|
||||
|
||||
test('unregistering owner or handler orphans claim and prevents dispatch', async () => {
|
||||
const window = loadCapabilities();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
api.registerParticipant('stems', { stems: { roles: ['owner'], commands: ['mute'], handlers: { mute: () => ({ outcome: 'handled' }) }, runtime: true } });
|
||||
api.claim({ capability: 'stems', claimId: 'nam.amp-active', requester: 'nam_tone' });
|
||||
api.unregisterParticipant('stems');
|
||||
@@ -51,7 +51,7 @@ test('unregistering owner or handler orphans claim and prevents dispatch', async
|
||||
|
||||
test('runtime enable disable is lifecycle state rather than user override', () => {
|
||||
const window = loadCapabilities();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
api.registerParticipant('plugin_a', { stems: { roles: ['provider'], commands: ['inspect'], runtime: true } });
|
||||
const disabled = api.setParticipantEnabled('plugin_a', 'stems', false, { requester: 'test' });
|
||||
const enabled = api.setParticipantEnabled('plugin_a', 'stems', true, { requester: 'test' });
|
||||
@@ -65,7 +65,7 @@ test('runtime enable disable is lifecycle state rather than user override', () =
|
||||
|
||||
test('failed no-op registrations do not block reload and rehydrate replacement', async () => {
|
||||
const window = loadCapabilities();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
api.registerParticipant('', { stems: { roles: ['owner'] } });
|
||||
api.registerParticipant('stems', { stems: { roles: ['owner'], commands: ['mute'], handlers: { mute: () => ({ outcome: 'handled', payload: { generation: 1 } }) }, runtime: true } });
|
||||
api.unregisterParticipant('stems');
|
||||
|
||||
@@ -12,7 +12,7 @@ function fixture(name) {
|
||||
|
||||
test('manifest participants are visible before runtime handlers register', () => {
|
||||
const window = loadCapabilities();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
api.registerParticipants([fixture('valid_owner_provider.json'), fixture('valid_requester_observer.json')]);
|
||||
|
||||
const stems = api.inspect('stems');
|
||||
@@ -27,7 +27,7 @@ test('manifest participants are visible before runtime handlers register', () =>
|
||||
|
||||
test('runtime registration refreshes an existing manifest participant without duplicating it', async () => {
|
||||
const window = loadCapabilities();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
api.registerParticipants([fixture('valid_owner_provider.json')]);
|
||||
api.registerParticipant('stems', {
|
||||
capabilities: {
|
||||
@@ -74,7 +74,7 @@ test('native library provider capability coordinates providers', async () => {
|
||||
}
|
||||
throw new Error(`unexpected fetch ${text}`);
|
||||
};
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
const events = [];
|
||||
api.subscribe('library:source-changed', event => events.push(event));
|
||||
|
||||
@@ -101,7 +101,7 @@ test('native library provider capability coordinates providers', async () => {
|
||||
assert.equal(selected.payload.current, 'remote:frodo');
|
||||
assert.equal(events.length, 1);
|
||||
assert.equal(events[0].payload.to, 'remote:frodo');
|
||||
assert.equal(window.slopsmith.libraryProviders.snapshot().current, 'remote:frodo');
|
||||
assert.equal(window.feedBack.libraryProviders.snapshot().current, 'remote:frodo');
|
||||
|
||||
const syncResult = await api.command('library', 'sync-song', {
|
||||
requester: 'test',
|
||||
@@ -123,7 +123,7 @@ test('removed library providers are unregistered as library participants on refr
|
||||
if (String(url) === '/api/library/providers') return { ok: true, json: async () => ({ providers: providerSet }) };
|
||||
throw new Error(`unexpected fetch ${url}`);
|
||||
};
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
|
||||
await api.command('library', 'refresh-providers', { requester: 'test' });
|
||||
let ids = api.inspect('library').participants.map(p => p.pluginId);
|
||||
@@ -160,7 +160,7 @@ test('a plugin with non-provider library roles is not wiped when its provider di
|
||||
if (String(url) === '/api/library/providers') return { ok: true, json: async () => ({ providers: providerSet }) };
|
||||
throw new Error(`unexpected fetch ${url}`);
|
||||
};
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
|
||||
// The same plugin also declares an observer library role via its manifest —
|
||||
// legitimate participation the provider-cleanup path must not delete.
|
||||
@@ -187,7 +187,7 @@ test('a plugin with non-provider library roles is not wiped when its provider di
|
||||
|
||||
test('runtime domain library declarations appear as library participants', () => {
|
||||
const window = loadCapabilities();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
|
||||
const touched = api.registerParticipants([{
|
||||
id: 'remote-library-client',
|
||||
|
||||
@@ -4,7 +4,7 @@ const { loadCapabilities } = require('./capabilities_test_harness');
|
||||
|
||||
test('multi-provider participants use deterministic order without duplicate-owner conflict', async () => {
|
||||
const window = loadCapabilities();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
const calls = [];
|
||||
api.registerParticipant('provider_b', { 'shared-viz': { roles: ['owner', 'provider'], ownership: 'multi-provider', commands: ['register-provider'], order: { after: ['provider_a'] }, handlers: { 'register-provider': () => { calls.push('b'); return { outcome: 'passed' }; } }, runtime: true } });
|
||||
api.registerParticipant('provider_a', { 'shared-viz': { roles: ['owner', 'provider'], ownership: 'multi-provider', commands: ['register-provider'], handlers: { 'register-provider': () => { calls.push('a'); return { outcome: 'handled' }; } }, runtime: true } });
|
||||
|
||||
@@ -4,7 +4,7 @@ const { loadCapabilities } = require('./capabilities_test_harness');
|
||||
|
||||
test('exclusive duplicate owners report conflict and degrade dispatch', async () => {
|
||||
const window = loadCapabilities();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
api.registerParticipant('owner_a', { stems: { roles: ['owner'], commands: ['mute'], handlers: { mute: () => ({ outcome: 'handled' }) }, runtime: true } });
|
||||
api.registerParticipant('owner_b', { stems: { roles: ['owner'], commands: ['mute'], handlers: { mute: () => ({ outcome: 'handled' }) }, runtime: true } });
|
||||
|
||||
@@ -18,7 +18,7 @@ test('exclusive duplicate owners report conflict and degrade dispatch', async ()
|
||||
|
||||
test('no-owner no-handler and unsupported-command outcomes are explicit', async () => {
|
||||
const window = loadCapabilities();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
let result = await api.dispatch({ capability: 'missing-domain', command: 'mute', source: 'test' });
|
||||
assert.equal(result.status, 'no-owner');
|
||||
assert.equal(result.outcome, 'no-owner');
|
||||
|
||||
@@ -45,7 +45,7 @@ function createWindow(options = {}) {
|
||||
document: {
|
||||
getElementById(id) { return elements.get(id) || null; },
|
||||
},
|
||||
slopsmith: {
|
||||
feedBack: {
|
||||
emit(type, detail) {
|
||||
window.dispatchEvent(new CustomEvent(type, { detail }));
|
||||
},
|
||||
|
||||
@@ -6,7 +6,7 @@ const { loadCapabilities, ROOT } = require('./capabilities_test_harness');
|
||||
|
||||
test('unsupported capability-pipelines versions are incompatible and do not execute handlers', async () => {
|
||||
const window = loadCapabilities();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
const fixture = JSON.parse(fs.readFileSync(path.join(ROOT, 'tests', 'fixtures', 'plugin_capabilities', 'unsupported_capability_version.json'), 'utf8'));
|
||||
let invoked = false;
|
||||
fixture.capabilities.stems.handlers = { mute: () => { invoked = true; return { outcome: 'handled' }; } };
|
||||
|
||||
@@ -58,7 +58,7 @@ function loadInspector(snapshot, options = {}) {
|
||||
for (const handler of (listeners.get(event.type) || []).slice()) handler(event);
|
||||
return true;
|
||||
},
|
||||
slopsmith: {
|
||||
feedBack: {
|
||||
capabilities: {
|
||||
snapshotDiagnostics: () => (typeof snapshot === 'function' ? snapshot() : snapshot),
|
||||
},
|
||||
@@ -90,7 +90,7 @@ test('capability inspector renders playback session route loop bridges and outco
|
||||
expectedCompatibilityShims: [],
|
||||
};
|
||||
const playbackSnapshot = {
|
||||
schema: 'slopsmith.playback.diagnostics.v1',
|
||||
schema: 'feedBack.playback.diagnostics.v1',
|
||||
state: {
|
||||
sessionId: 'playback-1',
|
||||
state: 'playing',
|
||||
@@ -158,8 +158,8 @@ test('capability inspector renders shims inside their capability domain', () =>
|
||||
lastHitAt: '2026-05-24T00:00:00.000Z',
|
||||
},
|
||||
{
|
||||
shimId: 'runtime:library:refresh:window.slopsmith.libraryProviders.refresh',
|
||||
source: 'window.slopsmith.libraryProviders.refresh',
|
||||
shimId: 'runtime:library:refresh:window.feedBack.libraryProviders.refresh',
|
||||
source: 'window.feedBack.libraryProviders.refresh',
|
||||
capability: 'library',
|
||||
legacySurface: 'refresh',
|
||||
status: 'used',
|
||||
@@ -243,8 +243,8 @@ test('capability inspector renders shims inside their capability domain', () =>
|
||||
assert.doesNotMatch(content, /id="capability-domain-library-graph-frame"/);
|
||||
assert.doesNotMatch(content, /Domain summary/);
|
||||
|
||||
window.__slopsmithCapabilityInspector.expandedDomains = { library: true, playback: true, 'custom.practice': true };
|
||||
window.__slopsmithCapabilityInspector.render();
|
||||
window.__feedBackCapabilityInspector.expandedDomains = { library: true, playback: true, 'custom.practice': true };
|
||||
window.__feedBackCapabilityInspector.render();
|
||||
const expandedContent = elements.get('capability-inspector-content').innerHTML;
|
||||
|
||||
assert.match(expandedContent, /aria-expanded="true"/);
|
||||
@@ -307,7 +307,7 @@ test('capability inspector renders shims inside their capability domain', () =>
|
||||
assert.doesNotMatch(expandedContent, /data-copy-surface=/);
|
||||
|
||||
filterElement.value = 'playback';
|
||||
window.__slopsmithCapabilityInspector.render();
|
||||
window.__feedBackCapabilityInspector.render();
|
||||
const selectedContent = elements.get('capability-inspector-content').innerHTML;
|
||||
|
||||
assert.match(selectedContent, /data-domain-graph="playback"/);
|
||||
@@ -400,11 +400,11 @@ test('capability inspector renders shims inside their capability domain', () =>
|
||||
assert.ok(selectedContent.indexOf('data-legend-icon="operation"') < selectedContent.indexOf('data-legend-icon="command"'));
|
||||
assert.doesNotMatch(selectedContent, /Domain group/);
|
||||
|
||||
window.__slopsmithCapabilityInspector.graphCollapsedGroups = {
|
||||
window.__feedBackCapabilityInspector.graphCollapsedGroups = {
|
||||
'playback|provider|all|command': true,
|
||||
'playback|participant|2|event': true,
|
||||
};
|
||||
window.__slopsmithCapabilityInspector.render();
|
||||
window.__feedBackCapabilityInspector.render();
|
||||
const collapsedGroupContent = elements.get('capability-inspector-content').innerHTML;
|
||||
assert.match(collapsedGroupContent, /data-graph-group-collapsed="true"/);
|
||||
assert.match(collapsedGroupContent, /data-graph-capability-port="group:command"/);
|
||||
@@ -416,12 +416,12 @@ test('capability inspector renders shims inside their capability domain', () =>
|
||||
assert.doesNotMatch(collapsedParticipantButton, /<svg class="h-3\.5 w-3\.5/);
|
||||
assert.doesNotMatch(collapsedParticipantButton, /<path d="m9 6 6 6-6 6"\/>/);
|
||||
|
||||
window.__slopsmithCapabilityInspector.graphCollapsedGroups = {};
|
||||
window.__slopsmithCapabilityInspector.render();
|
||||
window.__feedBackCapabilityInspector.graphCollapsedGroups = {};
|
||||
window.__feedBackCapabilityInspector.render();
|
||||
|
||||
filterElement.value = 'library';
|
||||
window.__slopsmithCapabilityInspector.domainGraphFilter = 'operations';
|
||||
window.__slopsmithCapabilityInspector.render();
|
||||
window.__feedBackCapabilityInspector.domainGraphFilter = 'operations';
|
||||
window.__feedBackCapabilityInspector.render();
|
||||
const operationsOnlyContent = elements.get('capability-inspector-content').innerHTML;
|
||||
assert.match(operationsOnlyContent, /aria-pressed="true" class="rounded border px-2 py-1 transition bg-purple-500\/40 text-white border-purple-400\/40">Operations/);
|
||||
assert.match(operationsOnlyContent, /data-capability-node="operation:query-page"/);
|
||||
@@ -430,25 +430,25 @@ test('capability inspector renders shims inside their capability domain', () =>
|
||||
assert.doesNotMatch(operationsOnlyContent, /data-capability-node="event:providers-refreshed"/);
|
||||
|
||||
filterElement.value = 'playback';
|
||||
window.__slopsmithCapabilityInspector.domainGraphFilter = 'events';
|
||||
window.__slopsmithCapabilityInspector.render();
|
||||
window.__feedBackCapabilityInspector.domainGraphFilter = 'events';
|
||||
window.__feedBackCapabilityInspector.render();
|
||||
const eventsOnlyContent = elements.get('capability-inspector-content').innerHTML;
|
||||
assert.match(eventsOnlyContent, /aria-pressed="true" class="rounded border px-2 py-1 transition bg-purple-500\/40 text-white border-purple-400\/40">Events/);
|
||||
assert.match(eventsOnlyContent, /data-capability-node="event:song:ready"/);
|
||||
assert.doesNotMatch(eventsOnlyContent, /data-capability-node="command:play"/);
|
||||
|
||||
filterElement.value = 'library';
|
||||
window.__slopsmithCapabilityInspector.domainGraphFilter = 'shimmed';
|
||||
window.__slopsmithCapabilityInspector.render();
|
||||
window.__feedBackCapabilityInspector.domainGraphFilter = 'shimmed';
|
||||
window.__feedBackCapabilityInspector.render();
|
||||
const shimmedOnlyContent = elements.get('capability-inspector-content').innerHTML;
|
||||
assert.match(shimmedOnlyContent, /data-link-kind="shimmed"/);
|
||||
assert.doesNotMatch(shimmedOnlyContent, /data-link-kind="observed"/);
|
||||
assert.match(shimmedOnlyContent, /title="4 participants" aria-label="4 participants" role="img"/);
|
||||
|
||||
filterElement.value = '';
|
||||
window.__slopsmithCapabilityInspector.expandedDomains = {};
|
||||
window.__slopsmithCapabilityInspector.domainGraphFilter = 'all';
|
||||
window.__slopsmithCapabilityInspector.render();
|
||||
window.__feedBackCapabilityInspector.expandedDomains = {};
|
||||
window.__feedBackCapabilityInspector.domainGraphFilter = 'all';
|
||||
window.__feedBackCapabilityInspector.render();
|
||||
const collapsedFilteredContent = elements.get('capability-inspector-content').innerHTML;
|
||||
assert.match(collapsedFilteredContent, /data-domain-graph="playback" data-domain-graph-expanded="false"[\s\S]*?title="3 participants" aria-label="3 participants" role="img"/);
|
||||
});
|
||||
@@ -468,7 +468,7 @@ test('capability inspector drops stale future-domain filter options', () => {
|
||||
|
||||
filter.innerHTML += '<option value="ui.player-panels">ui.player-panels</option>';
|
||||
filter.value = 'ui.player-panels';
|
||||
window.__slopsmithCapabilityInspector.render();
|
||||
window.__feedBackCapabilityInspector.render();
|
||||
|
||||
assert.equal(filter.value, '');
|
||||
assert.doesNotMatch(filter.innerHTML, /ui\.player-panels/);
|
||||
@@ -492,9 +492,9 @@ test('capability inspector refreshes collapsed counts after runtime capability c
|
||||
|
||||
currentSnapshot = {
|
||||
...currentSnapshot,
|
||||
participants: [{ pluginId: 'core' }, { pluginId: 'window.slopsmith.libraryProviders.refresh' }, { pluginId: 'remote_library_client' }],
|
||||
participants: [{ pluginId: 'core' }, { pluginId: 'window.feedBack.libraryProviders.refresh' }, { pluginId: 'remote_library_client' }],
|
||||
compatibilityShims: [
|
||||
{ shimId: 'runtime:library:refresh:window.slopsmith.libraryProviders.refresh', source: 'window.slopsmith.libraryProviders.refresh', capability: 'library', legacySurface: 'refresh', status: 'used', hitCount: 1 },
|
||||
{ shimId: 'runtime:library:refresh:window.feedBack.libraryProviders.refresh', source: 'window.feedBack.libraryProviders.refresh', capability: 'library', legacySurface: 'refresh', status: 'used', hitCount: 1 },
|
||||
{ shimId: 'runtime:library:select:remote_library_client', source: 'remote_library_client', capability: 'library', legacySurface: 'select', status: 'used', hitCount: 1 },
|
||||
],
|
||||
expectedCompatibilityShims: [
|
||||
@@ -502,7 +502,7 @@ test('capability inspector refreshes collapsed counts after runtime capability c
|
||||
{ capability: 'library', legacySurface: 'select', reason: 'legacy library provider selector calls are counted as library.select command use' },
|
||||
],
|
||||
};
|
||||
window.dispatchEvent(new window.CustomEvent('slopsmith:capabilities:changed'));
|
||||
window.dispatchEvent(new window.CustomEvent('feedBack:capabilities:changed'));
|
||||
|
||||
const refreshedContent = elements.get('capability-inspector-content').innerHTML;
|
||||
assert.match(refreshedContent, /data-domain-graph="library" data-domain-graph-expanded="false"[\s\S]*?title="2 participants"/);
|
||||
@@ -518,7 +518,7 @@ test('capability inspector links library legacy command surfaces to canonical en
|
||||
],
|
||||
participants: [{ pluginId: 'core' }],
|
||||
compatibilityShims: [
|
||||
{ shimId: 'runtime:library:refresh:window.slopsmith.libraryProviders.refresh', source: 'window.slopsmith.libraryProviders.refresh', capability: 'library', legacySurface: 'refresh', status: 'used', hitCount: 1 },
|
||||
{ shimId: 'runtime:library:refresh:window.feedBack.libraryProviders.refresh', source: 'window.feedBack.libraryProviders.refresh', capability: 'library', legacySurface: 'refresh', status: 'used', hitCount: 1 },
|
||||
{ shimId: 'runtime:library:select:remote_library_client', source: 'remote_library_client', capability: 'library', legacySurface: 'select', status: 'used', hitCount: 1 },
|
||||
{ shimId: 'runtime:library:sync-song:remote_library_client', source: 'remote_library_client', capability: 'library', legacySurface: 'sync-song', status: 'used', hitCount: 1 },
|
||||
],
|
||||
@@ -532,9 +532,9 @@ test('capability inspector links library legacy command surfaces to canonical en
|
||||
const filter = elements.get('capability-inspector-filter');
|
||||
|
||||
filter.value = 'library';
|
||||
window.__slopsmithCapabilityInspector.render();
|
||||
window.__feedBackCapabilityInspector.render();
|
||||
const libraryContent = elements.get('capability-inspector-content').innerHTML;
|
||||
assert.match(libraryContent, /data-domain-participant-card="window\.slopsmith\.libraryProviders\.refresh"/);
|
||||
assert.match(libraryContent, /data-domain-participant-card="window\.feedBack\.libraryProviders\.refresh"/);
|
||||
assert.match(libraryContent, /data-domain-participant-card="remote_library_client"/);
|
||||
assert.match(libraryContent, /data-link-kind="shimmed"[^>]*>refresh<\/span>/);
|
||||
assert.match(libraryContent, /data-link-kind="shimmed"[^>]*>select<\/span>/);
|
||||
@@ -575,7 +575,7 @@ test('selected domain participant count includes shim-only graph participants',
|
||||
],
|
||||
participants: [{ pluginId: 'core' }],
|
||||
compatibilityShims: [
|
||||
{ shimId: 'runtime:library:refresh:window.slopsmith.libraryProviders.refresh', source: 'window.slopsmith.libraryProviders.refresh', capability: 'library', legacySurface: 'refresh', status: 'used', hitCount: 1 },
|
||||
{ shimId: 'runtime:library:refresh:window.feedBack.libraryProviders.refresh', source: 'window.feedBack.libraryProviders.refresh', capability: 'library', legacySurface: 'refresh', status: 'used', hitCount: 1 },
|
||||
{ shimId: 'runtime:library:select:remote_library_client', source: 'remote_library_client', capability: 'library', legacySurface: 'select', status: 'used', hitCount: 1 },
|
||||
],
|
||||
expectedCompatibilityShims: [],
|
||||
@@ -583,12 +583,12 @@ test('selected domain participant count includes shim-only graph participants',
|
||||
const { window, elements } = loadInspector(snapshot);
|
||||
const filter = elements.get('capability-inspector-filter');
|
||||
filter.value = 'library';
|
||||
window.__slopsmithCapabilityInspector.render();
|
||||
window.__feedBackCapabilityInspector.render();
|
||||
const selectedContent = elements.get('capability-inspector-content').innerHTML;
|
||||
|
||||
assert.match(selectedContent, /title="2 participants"/);
|
||||
assert.doesNotMatch(selectedContent, /2 Participants/);
|
||||
assert.match(selectedContent, /data-domain-participant-card="window\.slopsmith\.libraryProviders\.refresh"/);
|
||||
assert.match(selectedContent, /data-domain-participant-card="window\.feedBack\.libraryProviders\.refresh"/);
|
||||
assert.match(selectedContent, /data-domain-participant-card="remote_library_client"/);
|
||||
});
|
||||
|
||||
@@ -604,9 +604,9 @@ test('capability inspector renders audio-mix fader diagnostics from audio-sessio
|
||||
expectedCompatibilityShims: [],
|
||||
};
|
||||
const { window, elements } = loadInspector(snapshot);
|
||||
window.slopsmith.audioSession = {
|
||||
window.feedBack.audioSession = {
|
||||
snapshot: () => ({
|
||||
schema: 'slopsmith.audio_session.diagnostics.v1',
|
||||
schema: 'feedBack.audio_session.diagnostics.v1',
|
||||
session: { route: { routeKind: 'desktop', availability: 'degraded' }, analyser: { source: 'plugin', availability: 'available' } },
|
||||
domains: {
|
||||
'audio-mix': {
|
||||
@@ -627,7 +627,7 @@ test('capability inspector renders audio-mix fader diagnostics from audio-sessio
|
||||
}),
|
||||
};
|
||||
|
||||
window.__slopsmithCapabilityInspector.render();
|
||||
window.__feedBackCapabilityInspector.render();
|
||||
const content = elements.get('capability-inspector-content').innerHTML;
|
||||
|
||||
assert.match(content, /data-audio-session-support/);
|
||||
@@ -649,9 +649,9 @@ test('capability inspector renders audio-input sources selection sessions bridge
|
||||
expectedCompatibilityShims: [],
|
||||
};
|
||||
const { window, elements } = loadInspector(snapshot);
|
||||
window.slopsmith.audioSession = {
|
||||
window.feedBack.audioSession = {
|
||||
snapshot: () => ({
|
||||
schema: 'slopsmith.audio_session.diagnostics.v1',
|
||||
schema: 'feedBack.audio_session.diagnostics.v1',
|
||||
session: { route: { routeKind: 'html5', availability: 'available' }, analyser: { source: 'none', availability: 'unavailable' } },
|
||||
domains: {
|
||||
'audio-mix': { participants: [], faders: [], route: { routeKind: 'html5', availability: 'available' }, analyser: { source: 'none', availability: 'unavailable' }, bridges: [] },
|
||||
@@ -673,7 +673,7 @@ test('capability inspector renders audio-input sources selection sessions bridge
|
||||
}),
|
||||
};
|
||||
|
||||
window.__slopsmithCapabilityInspector.render();
|
||||
window.__feedBackCapabilityInspector.render();
|
||||
const content = elements.get('capability-inspector-content').innerHTML;
|
||||
|
||||
assert.match(content, /Input: Input 1:available:mono:native, Legacy Input:available:stereo:compatibility:superseded/);
|
||||
@@ -695,9 +695,9 @@ test('capability inspector renders audio-monitoring providers sessions direct mo
|
||||
expectedCompatibilityShims: [],
|
||||
};
|
||||
const { window, elements } = loadInspector(snapshot);
|
||||
window.slopsmith.audioSession = {
|
||||
window.feedBack.audioSession = {
|
||||
snapshot: () => ({
|
||||
schema: 'slopsmith.audio_session.diagnostics.v1',
|
||||
schema: 'feedBack.audio_session.diagnostics.v1',
|
||||
session: { route: { routeKind: 'desktop', availability: 'available' }, analyser: { source: 'plugin', availability: 'available' } },
|
||||
domains: {
|
||||
'audio-mix': { participants: [], faders: [], route: { routeKind: 'desktop', availability: 'available' }, analyser: { source: 'plugin', availability: 'available' }, bridges: [] },
|
||||
@@ -726,7 +726,7 @@ test('capability inspector renders audio-monitoring providers sessions direct mo
|
||||
}),
|
||||
};
|
||||
|
||||
window.__slopsmithCapabilityInspector.render();
|
||||
window.__feedBackCapabilityInspector.render();
|
||||
const content = elements.get('capability-inspector-content').innerHTML;
|
||||
|
||||
assert.match(content, /Monitoring providers: Native Monitor:available:native, Legacy Monitor:available:compatibility:superseded/);
|
||||
|
||||
@@ -7,9 +7,9 @@ const { createWindow, ROOT } = require('./capabilities_test_harness');
|
||||
|
||||
function loadDiagnostics() {
|
||||
const window = createWindow();
|
||||
// diagnostics.js short-circuits if window.slopsmith.diagnostics already
|
||||
// diagnostics.js short-circuits if window.feedBack.diagnostics already
|
||||
// exists (idempotent guard); the harness stubs it, so clear it first.
|
||||
window.slopsmith.diagnostics = undefined;
|
||||
window.feedBack.diagnostics = undefined;
|
||||
window.navigator = { userAgent: 'test' };
|
||||
const context = vm.createContext(window);
|
||||
const source = fs.readFileSync(path.join(ROOT, 'static', 'diagnostics.js'), 'utf8');
|
||||
@@ -18,7 +18,7 @@ function loadDiagnostics() {
|
||||
}
|
||||
|
||||
test('summarizeRuntimeDomains counts actual UI contributions, not the {declared,legacy} wrapper keys', () => {
|
||||
const { summarizeRuntimeDomains } = loadDiagnostics().slopsmith.diagnostics;
|
||||
const { summarizeRuntimeDomains } = loadDiagnostics().feedBack.diagnostics;
|
||||
const snapshot = {
|
||||
plugins: [
|
||||
{
|
||||
@@ -49,7 +49,7 @@ test('summarizeRuntimeDomains counts actual UI contributions, not the {declared,
|
||||
});
|
||||
|
||||
test('summarizeRuntimeDomains tolerates a flat region→contributions map', () => {
|
||||
const { summarizeRuntimeDomains } = loadDiagnostics().slopsmith.diagnostics;
|
||||
const { summarizeRuntimeDomains } = loadDiagnostics().feedBack.diagnostics;
|
||||
const summary = summarizeRuntimeDomains({
|
||||
plugins: [{ ui_contributions: { 'ui.navigation': [{ id: 'a' }, { id: 'b' }] } }],
|
||||
});
|
||||
@@ -57,7 +57,7 @@ test('summarizeRuntimeDomains tolerates a flat region→contributions map', () =
|
||||
});
|
||||
|
||||
test('summarizeRuntimeDomains counts only array region values (malformed values are ignored)', () => {
|
||||
const { summarizeRuntimeDomains } = loadDiagnostics().slopsmith.diagnostics;
|
||||
const { summarizeRuntimeDomains } = loadDiagnostics().feedBack.diagnostics;
|
||||
// Non-array region values (a stray object/string from a malformed payload)
|
||||
// must not inflate the count — only the two real array entries count.
|
||||
const declaredSummary = summarizeRuntimeDomains({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Pins the arpeggio chord-gem deferral gating in plugins/highway_3d/screen.js
|
||||
// (slopsmith#262). Without these guards, an over-eager `deferChordGems` makes
|
||||
// (feedBack#262). Without these guards, an over-eager `deferChordGems` makes
|
||||
// arpeggio frames empty when standalone notes don't actually cover the shape,
|
||||
// and an under-eager one duplicates gems on top of the standalone passage.
|
||||
//
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Pins 3D Highway left-handed fret ordering (slopsmith#321).
|
||||
// Pins 3D Highway left-handed fret ordering (feedBack#321).
|
||||
// Source-level only, matching the other tests/js/ regression guards: the
|
||||
// runtime path is browser/WebGL-heavy, so these tests preserve the exact
|
||||
// contracts that keep the lefty geometry coherent.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Contract test for 3D Highway per-panel control metadata (slopsmith#247).
|
||||
// Contract test for 3D Highway per-panel control metadata (feedBack#247).
|
||||
// The plugin script is evaluated in a vm sandbox so factory statics are
|
||||
// tested without constructing a renderer instance or calling init().
|
||||
|
||||
@@ -22,7 +22,7 @@ function loadHighway3dStatics() {
|
||||
// semantic anchor inside the IIFE — so harmless footer edits (a trailing
|
||||
// sourceMappingURL comment, extra whitespace, a different IIFE close
|
||||
// style) do not break this contract test.
|
||||
const ANCHOR = 'window.slopsmithViz_highway_3d = createFactory;';
|
||||
const ANCHOR = 'window.feedBackViz_highway_3d = createFactory;';
|
||||
assert.equal(
|
||||
src.split(ANCHOR).length - 1,
|
||||
1,
|
||||
@@ -46,7 +46,7 @@ function loadHighway3dStatics() {
|
||||
},
|
||||
performance: { now: () => 0 },
|
||||
window: {
|
||||
slopsmithTour: {
|
||||
feedBackTour: {
|
||||
register() {},
|
||||
},
|
||||
},
|
||||
@@ -79,7 +79,7 @@ function assertOptionObject(option, controlKey) {
|
||||
|
||||
test('3D Highway exposes static panelControls descriptors for per-panel hosts', () => {
|
||||
const window = loadHighway3dStatics();
|
||||
const factory = window.slopsmithViz_highway_3d;
|
||||
const factory = window.feedBackViz_highway_3d;
|
||||
assert.equal(typeof factory, 'function', 'screen.js must register the 3D Highway factory');
|
||||
|
||||
assert.ok(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Source-level guards for the pool().warm() helper added in
|
||||
// slopsmith#226 — locks in:
|
||||
// feedBack#226 — locks in:
|
||||
// 1. The factory exposes .warm() (so future refactors don't quietly
|
||||
// remove the boardInit pre-allocation strategy).
|
||||
// 2. warm() coerces its argument via `cap | 0` + `Math.max(0, …)` so a
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
// pause the 3D highway crept forward against a frozen audio clock and then
|
||||
// snapped back to raw — a visible twitch on every pause.
|
||||
//
|
||||
// The fix wires a host pause signal (slopsmith core's bundle.isPlaying) into
|
||||
// The fix wires a host pause signal (feedBack core's bundle.isPlaying) into
|
||||
// smoothNow: when the chart clock is not advancing, return raw immediately
|
||||
// and re-anchor. These tests lock in both halves of the contract by
|
||||
// inspecting source (the createHighway / renderer closures own WebGL + audio
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Source-level guards for the load-adaptive render scale (slopsmith#654).
|
||||
// Source-level guards for the load-adaptive render scale (feedBack#654).
|
||||
// The createHighway closure owns the rAF loop + WebGL sizing that's too
|
||||
// heavy for a vm sandbox, so — like highway_visibility.test.js — these
|
||||
// lock in the wiring rather than execute it.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Pins the native-audio barrier ordering in static/highway.js
|
||||
// (slopsmith-desktop#117). The highway must await window.slopsmithAudioBarrier
|
||||
// (feedBack-desktop#117). The highway must await window.feedBackAudioBarrier
|
||||
// before touching the JUCE backing engine, otherwise a NAM tone graph build
|
||||
// that restarts the native audio device races the backing-track load.
|
||||
//
|
||||
@@ -12,13 +12,13 @@ const path = require('node:path');
|
||||
|
||||
const HIGHWAY_JS = path.join(__dirname, '..', '..', 'static', 'highway.js');
|
||||
|
||||
test('highway awaits slopsmithAudioBarrier before the JUCE backing path', () => {
|
||||
test('highway awaits feedBackAudioBarrier before the JUCE backing path', () => {
|
||||
const src = fs.readFileSync(HIGHWAY_JS, 'utf8');
|
||||
const barrierIdx = src.indexOf('window.slopsmithAudioBarrier');
|
||||
const barrierIdx = src.indexOf('window.feedBackAudioBarrier');
|
||||
const isRunningIdx = src.indexOf('juceApi.isAudioRunning()');
|
||||
const loadIdx = src.indexOf('juceApi.loadBackingTrack');
|
||||
|
||||
assert.ok(barrierIdx !== -1, 'highway must reference window.slopsmithAudioBarrier');
|
||||
assert.ok(barrierIdx !== -1, 'highway must reference window.feedBackAudioBarrier');
|
||||
assert.ok(isRunningIdx !== -1, 'highway must still call juceApi.isAudioRunning()');
|
||||
assert.ok(loadIdx !== -1, 'highway must still call juceApi.loadBackingTrack');
|
||||
assert.ok(barrierIdx < isRunningIdx,
|
||||
@@ -29,8 +29,8 @@ test('highway awaits slopsmithAudioBarrier before the JUCE backing path', () =>
|
||||
|
||||
test('the barrier await is timeout-guarded so a stuck plugin barrier cannot wedge song entry', () => {
|
||||
const src = fs.readFileSync(HIGHWAY_JS, 'utf8');
|
||||
const start = src.indexOf('window.slopsmithAudioBarrier');
|
||||
assert.ok(start !== -1, 'highway must reference window.slopsmithAudioBarrier');
|
||||
const start = src.indexOf('window.feedBackAudioBarrier');
|
||||
assert.ok(start !== -1, 'highway must reference window.feedBackAudioBarrier');
|
||||
const region = src.slice(start, start + 600);
|
||||
// Catching rejections alone does not cover a never-settling promise — the
|
||||
// await must be raced against a local timeout.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// regression that drops one of the three keys, or forgets to reset the
|
||||
// derived state, will fail in CI.
|
||||
//
|
||||
// Background: see slopsmith#412 and the Copilot review thread that
|
||||
// Background: see feedBack#412 and the Copilot review thread that
|
||||
// surfaced the `chordTemplates` ordering edge case (templates can land
|
||||
// after the final `chords` chunk; `isOpen()`-derived `nonZeroNotes`
|
||||
// would otherwise stay stale until the next chord transition).
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// The public plugin API: window.slopsmith.highwayColors. The facade is a thin,
|
||||
// The public plugin API: window.feedBack.highwayColors. The facade is a thin,
|
||||
// stable wrapper over the (private) string-color manager in app.js. These tests
|
||||
// extract _hwcInstallFacade and run it against a fake window/bus with stubbed
|
||||
// manager functions, so the documented surface + wiring are locked in.
|
||||
@@ -49,7 +49,7 @@ function buildFacade() {
|
||||
emit(e, d) { (listeners[e] || []).slice().forEach((f) => f({ detail: d })); },
|
||||
_count: (e) => (listeners[e] || []).length,
|
||||
};
|
||||
const win = { slopsmith: bus, highway: { getStringColors: () => ['#aaaaaa'] } };
|
||||
const win = { feedBack: bus, highway: { getStringColors: () => ['#aaaaaa'] } };
|
||||
const HWC_SLOTS = [
|
||||
{ key: 'highE', label: 'High E', sub: '1st' }, { key: 'B', label: 'B', sub: '2nd' },
|
||||
{ key: 'G', label: 'G', sub: '3rd' }, { key: 'D', label: 'D', sub: '4th' },
|
||||
@@ -74,7 +74,7 @@ function buildFacade() {
|
||||
stubs.applyHighwayStringColors, stubs.encodeHighwayColorShare, stubs.decodeHighwayColorShare,
|
||||
);
|
||||
installer();
|
||||
return { api: win.slopsmith.highwayColors, win, bus, calls, installer, stubs };
|
||||
return { api: win.feedBack.highwayColors, win, bus, calls, installer, stubs };
|
||||
}
|
||||
|
||||
test('initHighwayColors installs the facade', () => {
|
||||
@@ -159,5 +159,5 @@ test('repeated onChange with the same handler unsubscribes independently (no lea
|
||||
test('install is idempotent (does not replace an existing facade)', () => {
|
||||
const { api, win, installer } = buildFacade();
|
||||
installer();
|
||||
assert.equal(win.slopsmith.highwayColors, api, 'second install must be a no-op');
|
||||
assert.equal(win.feedBack.highwayColors, api, 'second install must be a no-op');
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Source-level guards for the per-note judgment hook (slopsmith#254):
|
||||
// Source-level guards for the per-note judgment hook (feedBack#254):
|
||||
// highway.setNoteStateProvider / getNoteStateProvider / getNoteState,
|
||||
// bundle.getNoteState, isDefaultRenderer, and the _noteState
|
||||
// normalization rules. The createHighway closure owns canvas + WebGL
|
||||
@@ -54,7 +54,7 @@ test('_makeBundle exposes getNoteState (stable reference, no per-frame alloc)',
|
||||
assert.match(fn, /getNoteState:\s*_noteState\b/, 'bundle.getNoteState must be the stable _noteState reference');
|
||||
});
|
||||
|
||||
test('_makeBundle exposes getNoteStateProvider as a stable reference (slopsmith#254)', () => {
|
||||
test('_makeBundle exposes getNoteStateProvider as a stable reference (feedBack#254)', () => {
|
||||
const src = fs.readFileSync(highwayJs, 'utf8');
|
||||
const fn = extractBlock(src, 'function _makeBundle()');
|
||||
// Same allocation discipline as getNoteState: highway_3d uses this
|
||||
@@ -106,7 +106,7 @@ test('3D highway captures bundle.getNoteState and overrides legacy hit/miss with
|
||||
assert.match(src, /const\s+_showHit\s*=\s*\(\s*_ndState\s*===\s*['"]miss['"]\s*\)\s*\?\s*false\s*:\s*\(\s*_ndState\s*\?\s*_ndGood\s*:\s*\(\s*hit\s*\|\|\s*\(\s*n\.f\s*>\s*0\s*&&\s*inGhostWin\s*\)\s*\)\s*\)/, '_showHit must honor a provider "miss" and fall back to the hit/ghost heuristic only with no verdict');
|
||||
});
|
||||
|
||||
test('3D highway captures _ndHasProvider via bundle.getNoteStateProvider (slopsmith#254)', () => {
|
||||
test('3D highway captures _ndHasProvider via bundle.getNoteStateProvider (feedBack#254)', () => {
|
||||
const src = fs.readFileSync(highway3dJs, 'utf8');
|
||||
// Detect-mode behavior — verdict-window cull extension, chord-frame
|
||||
// hold floor, and the smart drawNote cull — must be gated on a real
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Source-level guards for the playback-aware paused-render throttle
|
||||
// (slopsmith#654). The createHighway closure owns the rAF loop + WebGL
|
||||
// (feedBack#654). The createHighway closure owns the rAF loop + WebGL
|
||||
// context lifecycle that's too heavy to reproduce in a vm sandbox, so —
|
||||
// like highway_visibility.test.js — these checks lock in the wiring.
|
||||
|
||||
|
||||
@@ -101,8 +101,8 @@ test('app.js color manager name-maps to both highways, with identity no-op + bui
|
||||
const idfn = extractBlock(src, 'function _hwcMappingIsIdentity(sc, isBass)');
|
||||
assert.match(idfn, /return isBass \? sc <= 4 : sc <= 6/, 'identity must be 4-string bass / ≤6-string guitar');
|
||||
// Re-apply on song load (string count can change the slot→index mapping).
|
||||
assert.match(src, /window\.slopsmith\.on\('viz:renderer:ready', reapplyHighwayStringColors\)/, 'must re-apply when a viz renderer becomes ready');
|
||||
assert.match(src, /window\.slopsmith\.on\('song:loaded', reapplyHighwayStringColors\)/, 'must re-apply on song load');
|
||||
assert.match(src, /window\.feedBack\.on\('viz:renderer:ready', reapplyHighwayStringColors\)/, 'must re-apply when a viz renderer becomes ready');
|
||||
assert.match(src, /window\.feedBack\.on\('song:loaded', reapplyHighwayStringColors\)/, 'must re-apply on song load');
|
||||
});
|
||||
|
||||
// ── Executable: dim/bright derivation math ────────────────────────────────
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Source-level guards for the visibility-aware rAF skip and the
|
||||
// highway:visibility event (slopsmith#246). The createHighway closure
|
||||
// highway:visibility event (feedBack#246). The createHighway closure
|
||||
// owns the canvas + WebGL context lifecycle that's too heavy to
|
||||
// reproduce in a vm sandbox — these checks lock in the wiring instead.
|
||||
|
||||
@@ -52,7 +52,7 @@ test('_emitVisibilityIfChanged is transition-only (no per-frame spam)', () => {
|
||||
assert.match(fn, /_lastVisible\s*=\s*v/, 'must update _lastVisible after a transition');
|
||||
assert.match(
|
||||
fn,
|
||||
/window\.slopsmith\.emit\(\s*['"]highway:visibility['"][\s\S]*?visible:\s*v[\s\S]*?canvas/,
|
||||
/window\.feedBack\.emit\(\s*['"]highway:visibility['"][\s\S]*?visible:\s*v[\s\S]*?canvas/,
|
||||
'must emit highway:visibility with { visible, canvas }',
|
||||
);
|
||||
});
|
||||
@@ -74,7 +74,7 @@ test('rAF draw() loop calls _emitVisibilityIfChanged and skips when hidden', ()
|
||||
assert.ok(readyIdx < drawIdx, 'ready gate must run before renderer.draw');
|
||||
});
|
||||
|
||||
test('draw() keeps an active custom renderer painting through an override-hide (slopsmith#819)', () => {
|
||||
test('draw() keeps an active custom renderer painting through an override-hide (feedBack#819)', () => {
|
||||
// The `_rendering` decision must distinguish a renderer-set override-hide
|
||||
// (setVisible(false) — canvas occluded by an opaque overlay, but the
|
||||
// active custom renderer still paints its own surface, e.g. Tab View's
|
||||
@@ -140,12 +140,12 @@ test('3D Highway subscribes to highway:visibility and toggles wrap on hide', ()
|
||||
// Listener registration with the documented event name (in init).
|
||||
assert.match(
|
||||
initSceneBlock,
|
||||
/window\.slopsmith\.on\(\s*['"]highway:visibility['"]/,
|
||||
/window\.feedBack\.on\(\s*['"]highway:visibility['"]/,
|
||||
'initScene must subscribe to highway:visibility',
|
||||
);
|
||||
// Handler filters by canvas identity so splitscreen panels don't
|
||||
// hide each other's overlays — every instance receives every event
|
||||
// on the shared slopsmith bus, so this gate is essential.
|
||||
// on the shared feedBack bus, so this gate is essential.
|
||||
assert.match(
|
||||
initSceneBlock,
|
||||
/e\.detail\.canvas\s*!==\s*highwayCanvas/,
|
||||
@@ -170,7 +170,7 @@ test('3D Highway subscribes to highway:visibility and toggles wrap on hide', ()
|
||||
// context-type-driven canvas swap. Per CLAUDE.md plugin contract.
|
||||
assert.match(
|
||||
initSceneBlock,
|
||||
/window\.slopsmith\.on\(\s*['"]highway:canvas-replaced['"]/,
|
||||
/window\.feedBack\.on\(\s*['"]highway:canvas-replaced['"]/,
|
||||
'initScene must track canvas swaps so the visibility gate keeps matching',
|
||||
);
|
||||
assert.match(
|
||||
@@ -181,12 +181,12 @@ test('3D Highway subscribes to highway:visibility and toggles wrap on hide', ()
|
||||
// Teardown unbinds both listeners.
|
||||
assert.match(
|
||||
teardownBlock,
|
||||
/window\.slopsmith\.off\(\s*['"]highway:visibility['"]/,
|
||||
/window\.feedBack\.off\(\s*['"]highway:visibility['"]/,
|
||||
'teardown must unbind highway:visibility',
|
||||
);
|
||||
assert.match(
|
||||
teardownBlock,
|
||||
/window\.slopsmith\.off\(\s*['"]highway:canvas-replaced['"]/,
|
||||
/window\.feedBack\.off\(\s*['"]highway:canvas-replaced['"]/,
|
||||
'teardown must unbind highway:canvas-replaced',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -90,8 +90,8 @@ function makeSandbox({ isAudioRunning, loadBackingTrack }) {
|
||||
};
|
||||
sandbox.window = sandbox;
|
||||
sandbox.window.jucePlayer = jucePlayer;
|
||||
sandbox.window.slopsmithDesktop = { audio: juceApi };
|
||||
sandbox.window.slopsmith = { audio: {} };
|
||||
sandbox.window.feedBackDesktop = { audio: juceApi };
|
||||
sandbox.window.feedBack = { audio: {} };
|
||||
|
||||
const src = fs.readFileSync(APP_JS, 'utf8');
|
||||
const iife = extractWatcherIIFE(src);
|
||||
@@ -217,7 +217,7 @@ test('stale-abort during a swap-then-restore is NOT memoised as a JUCE reject',
|
||||
sb.window._currentSongAudio = snapA;
|
||||
|
||||
let firstLoad = true;
|
||||
sb.window.slopsmithDesktop.audio.loadBackingTrack = (p) => {
|
||||
sb.window.feedBackDesktop.audio.loadBackingTrack = (p) => {
|
||||
sb.__calls.loadBackingTrack.push(p);
|
||||
if (firstLoad) {
|
||||
firstLoad = false;
|
||||
@@ -353,7 +353,7 @@ test('song change mid-flight aborts the switch without mutating routing', async
|
||||
sb.window._currentSongAudio = original;
|
||||
// Swap the current song the moment loadBackingTrack is consulted, so the
|
||||
// post-await staleness check sees a different _currentSongAudio identity.
|
||||
sb.window.slopsmithDesktop.audio.loadBackingTrack = () => {
|
||||
sb.window.feedBackDesktop.audio.loadBackingTrack = () => {
|
||||
sb.window._currentSongAudio = { url: '/audio/song-b.ogg', juceEligible: true };
|
||||
return Promise.resolve(true);
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@ const { loadAudioSession } = require('./audio_session_test_harness');
|
||||
|
||||
test('active audio domains expose expected legacy shim metadata', () => {
|
||||
const window = loadAudioSession();
|
||||
const shims = window.slopsmith.capabilities.snapshotDiagnostics().compatibilityShims;
|
||||
const shims = window.feedBack.capabilities.snapshotDiagnostics().compatibilityShims;
|
||||
for (const shimId of ['audio-mix.fader-registry', 'audio-mix.song-volume', 'audio-mix.analyser', 'audio-input.legacy-source', 'audio-monitoring.audio-barrier', 'stems.master-volume', 'stems.private-state']) {
|
||||
assert.equal(shims.some(shim => shim.shimId === shimId && shim.status === 'active'), true, shimId);
|
||||
}
|
||||
@@ -14,12 +14,12 @@ test('active audio domains expose expected legacy shim metadata', () => {
|
||||
|
||||
test('legacy bridge hit counts are attributed to canonical audio domains', () => {
|
||||
const window = loadAudioSession();
|
||||
const audioSession = window.slopsmith.audioSession;
|
||||
const audioSession = window.feedBack.audioSession;
|
||||
audioSession.recordBridgeHit({ domain: 'audio-mix', bridgeId: 'audio-mix.analyser', legacySurface: 'HTMLAudioElement analyser tap', participantId: 'highway_3d' });
|
||||
audioSession.recordBridgeHit({ domain: 'audio-input', bridgeId: 'audio-input.legacy-source', legacySurface: 'navigator.mediaDevices.getUserMedia', participantId: 'note_detect' });
|
||||
audioSession.recordBridgeHit({ domain: 'audio-monitoring', bridgeId: 'audio-monitoring.audio-barrier', legacySurface: 'window.slopsmithAudioBarrier', participantId: 'note_detect' });
|
||||
audioSession.recordBridgeHit({ domain: 'audio-monitoring', bridgeId: 'audio-monitoring.audio-barrier', legacySurface: 'window.feedBackAudioBarrier', participantId: 'note_detect' });
|
||||
|
||||
const shims = window.slopsmith.capabilities.snapshotDiagnostics().compatibilityShims;
|
||||
const shims = window.feedBack.capabilities.snapshotDiagnostics().compatibilityShims;
|
||||
assert.equal(shims.find(shim => shim.shimId === 'audio-mix.analyser').hitCount, 1);
|
||||
assert.equal(shims.find(shim => shim.shimId === 'audio-input.legacy-source').capability, 'audio-input');
|
||||
assert.equal(shims.find(shim => shim.shimId === 'audio-monitoring.audio-barrier').hitCount, 1);
|
||||
@@ -30,9 +30,9 @@ test('native audio-mix participant suppresses matching legacy fader and records
|
||||
const window = loadAudioSession();
|
||||
installMixerDom(window);
|
||||
runBrowserScript(window, 'static/audio-mixer.js');
|
||||
window.slopsmith.audioSession.startSession({ sessionId: 'main:test-song' });
|
||||
window.feedBack.audioSession.startSession({ sessionId: 'main:test-song' });
|
||||
|
||||
window.slopsmith.audio.registerFader({
|
||||
window.feedBack.audio.registerFader({
|
||||
id: 'delay.wet',
|
||||
label: 'Delay Wet Legacy',
|
||||
min: 0,
|
||||
@@ -43,7 +43,7 @@ test('native audio-mix participant suppresses matching legacy fader and records
|
||||
getValue: () => 0.2,
|
||||
setValue: () => {},
|
||||
});
|
||||
window.slopsmith.audioSession.registerMixParticipant({
|
||||
window.feedBack.audioSession.registerMixParticipant({
|
||||
participantId: 'plugin.delay.native',
|
||||
ownerPluginId: 'delay',
|
||||
label: 'Delay Wet',
|
||||
@@ -54,8 +54,8 @@ test('native audio-mix participant suppresses matching legacy fader and records
|
||||
operations: ['fader.get-value', 'fader.set-value'],
|
||||
});
|
||||
|
||||
const listed = await window.slopsmith.capabilities.dispatch({ capability: 'audio-mix', command: 'list-faders', source: 'test' });
|
||||
const snapshot = window.slopsmith.audioSession.snapshot();
|
||||
const listed = await window.feedBack.capabilities.dispatch({ capability: 'audio-mix', command: 'list-faders', source: 'test' });
|
||||
const snapshot = window.feedBack.audioSession.snapshot();
|
||||
const legacy = snapshot.domains['audio-mix'].participants.find(participant => participant.participantId === 'fader.delay.wet');
|
||||
|
||||
assert.equal(listed.payload.faders.some(fader => fader.participantId === 'plugin.delay.native'), true);
|
||||
@@ -86,8 +86,8 @@ function region(src, needle, length = 1200) {
|
||||
test('plugin script hydration exposes the current plugin id for legacy registrations', () => {
|
||||
const src = source(APP_JS);
|
||||
const block = region(src, 'script.src = `/api/plugins/${plugin.id}/screen.js');
|
||||
assert.match(block, /window\.slopsmith\._loadingPluginId\s*=\s*plugin\.id/);
|
||||
assert.match(block, /delete\s+window\.slopsmith\._loadingPluginId/);
|
||||
assert.match(block, /window\.feedBack\._loadingPluginId\s*=\s*plugin\.id/);
|
||||
assert.match(block, /delete\s+window\.feedBack\._loadingPluginId/);
|
||||
});
|
||||
|
||||
test('library providers route through native library capability', () => {
|
||||
@@ -100,7 +100,7 @@ test('library providers route through native library capability', () => {
|
||||
assert.match(librarySrc, /capabilities\.registerOwner\(['"]library['"]/);
|
||||
assert.match(librarySrc, /kind:\s*['"]provider-coordinator['"]/);
|
||||
assert.match(librarySrc, /'library\.read': \['query-page', 'query-artists', 'query-stats', 'tuning-names'\]/);
|
||||
assert.match(librarySrc, /window\.slopsmith\.libraryProviders\s*=\s*providerApi/);
|
||||
assert.match(librarySrc, /window\.feedBack\.libraryProviders\s*=\s*providerApi/);
|
||||
assert.match(loader, /api\.refresh\(\{ restoreSaved \}\)/);
|
||||
assert.match(selector, /capabilityApi\.command\(['"]library['"],\s*['"]select-provider['"]/);
|
||||
assert.match(sync, /capabilityApi\.command\(['"]library['"],\s*['"]sync-song['"]/);
|
||||
|
||||
@@ -28,7 +28,7 @@ test('shouldSuppressMonitorMuteHint only for external modes', () => {
|
||||
});
|
||||
|
||||
test('labels include internal, external, and spark options', () => {
|
||||
assert.match(toneSource.LABELS.internal, /feed\[dB\]ack internal tone/i);
|
||||
assert.match(toneSource.LABELS.internal, /fee\[dB\]ack internal tone/i);
|
||||
assert.match(toneSource.LABELS.external_hardware, /External amp/i);
|
||||
assert.match(toneSource.LABELS.spark_control_x, /Spark LIVE/i);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Verify the plugin-facing loop API: setLoop / clearLoop / getLoop on
|
||||
// window.slopsmith, the input validation in setLoop, and the
|
||||
// window.feedBack, the input validation in setLoop, and the
|
||||
// loadSavedLoop refactor that funnels through setLoop.
|
||||
//
|
||||
// Same isolation strategy as loop_restart.test.js — extract relevant
|
||||
@@ -82,7 +82,7 @@ function buildSandbox() {
|
||||
// assert on the label text in these tests, so a stub is enough.
|
||||
formatTime: (s) => String(s),
|
||||
window: {
|
||||
slopsmith: {
|
||||
feedBack: {
|
||||
playback: {
|
||||
transportEvent: (...args) => transportEvents.push(args),
|
||||
},
|
||||
@@ -236,18 +236,18 @@ test('loop helpers emit transport snapshots by default and can suppress adapter
|
||||
assert.equal(sandbox.transportEvents.length, 0);
|
||||
});
|
||||
|
||||
test('window.slopsmith API surface declares setLoop/clearLoop/getLoop', () => {
|
||||
test('window.feedBack API surface declares setLoop/clearLoop/getLoop', () => {
|
||||
// Source-level assertion: the plugin-facing namespace must expose
|
||||
// these three methods. Catches a future contributor moving them or
|
||||
// renaming silently.
|
||||
const src = fs.readFileSync(APP_JS, 'utf8');
|
||||
// Find the slopsmith Object.assign block and check method presence.
|
||||
const m = src.match(/window\.slopsmith\s*=\s*Object\.assign\(_slopsmithBus,\s*\{([\s\S]*?)\}\);\s*if \(_slopsmithExisting/);
|
||||
assert.ok(m, 'slopsmith Object.assign block not found');
|
||||
// Find the feedBack Object.assign block and check method presence.
|
||||
const m = src.match(/window\.feedBack\s*=\s*Object\.assign\(_feedBackBus,\s*\{([\s\S]*?)\}\);\s*if \(_feedBackExisting/);
|
||||
assert.ok(m, 'feedBack Object.assign block not found');
|
||||
const block = m[1];
|
||||
assert.match(block, /setLoop\s*\(/, 'setLoop method missing from slopsmith API');
|
||||
assert.match(block, /clearLoop\s*\(/, 'clearLoop method missing from slopsmith API');
|
||||
assert.match(block, /getLoop\s*\(/, 'getLoop method missing from slopsmith API');
|
||||
assert.match(block, /setLoop\s*\(/, 'setLoop method missing from feedBack API');
|
||||
assert.match(block, /clearLoop\s*\(/, 'clearLoop method missing from feedBack API');
|
||||
assert.match(block, /getLoop\s*\(/, 'getLoop method missing from feedBack API');
|
||||
});
|
||||
|
||||
test('loadSavedLoop funnels through setLoop (no duplicated UI mutation)', () => {
|
||||
|
||||
@@ -72,7 +72,7 @@ function buildSandbox() {
|
||||
// we don't need them to fire — the emit happens before beginCount.
|
||||
setTimeout: () => 0,
|
||||
|
||||
// Stubbed slopsmith DOM dependencies.
|
||||
// Stubbed feedBack DOM dependencies.
|
||||
audio: { pause() {} },
|
||||
jucePlayer: { pause: () => Promise.resolve(), play: () => Promise.resolve(true) },
|
||||
highway: { setTime() {}, getBPM: () => 120 },
|
||||
@@ -98,7 +98,7 @@ function buildSandbox() {
|
||||
|
||||
// Spy: records every emit call so the test can assert.
|
||||
window: {
|
||||
slopsmith: {
|
||||
feedBack: {
|
||||
emit(event, detail) { emitCalls.push({ event, detail }); },
|
||||
isPlaying: false,
|
||||
},
|
||||
@@ -121,7 +121,7 @@ test('loop:restart fires once when wrap path runs', async () => {
|
||||
// accidental revert before we even run the behavior assertion.
|
||||
assert.match(
|
||||
startCountInSrc,
|
||||
/window\.slopsmith\.emit\(\s*['"]loop:restart['"]/,
|
||||
/window\.feedBack\.emit\(\s*['"]loop:restart['"]/,
|
||||
'startCountIn is missing the loop:restart emit',
|
||||
);
|
||||
|
||||
@@ -229,7 +229,7 @@ test('loop:restart fires after highway.setTime, before beginCount', () => {
|
||||
const setTimeIdx = setTimeMatches.length
|
||||
? wrapStart + setTimeMatches[setTimeMatches.length - 1].index
|
||||
: -1;
|
||||
const emitRel = wrapSlice.search(/window\.slopsmith\.emit\(\s*['"]loop:restart['"]/);
|
||||
const emitRel = wrapSlice.search(/window\.feedBack\.emit\(\s*['"]loop:restart['"]/);
|
||||
const emitIdx = emitRel === -1 ? -1 : wrapStart + emitRel;
|
||||
const afterEmit = emitIdx === -1 ? '' : fn.slice(emitIdx);
|
||||
const beginCallMatch = afterEmit.match(/(?<!function\s)\bbeginCount\s*\(/);
|
||||
|
||||
@@ -19,7 +19,7 @@ function loadMidiInput(options = {}) {
|
||||
// A fake provider whose enumerate/open/close are observable by the test.
|
||||
function fakeProvider(window, overrides = {}) {
|
||||
const calls = { enumerate: 0, open: [], close: [] };
|
||||
window.slopsmith.midiInput.registerProvider({
|
||||
window.feedBack.midiInput.registerProvider({
|
||||
providerId: 'web-midi',
|
||||
label: 'Web MIDI',
|
||||
participantId: 'input_setup',
|
||||
@@ -33,7 +33,7 @@ function fakeProvider(window, overrides = {}) {
|
||||
|
||||
test('midi-input registers an active sensitive provider-coordinator', () => {
|
||||
const window = loadMidiInput();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
const pipeline = api.inspect('midi-input');
|
||||
assert.ok(pipeline, 'midi-input pipeline exists');
|
||||
const owner = (pipeline.participants || []).find(p => p.pluginId === 'core.midi-input');
|
||||
@@ -43,12 +43,12 @@ test('midi-input registers an active sensitive provider-coordinator', () => {
|
||||
for (const cmd of ['inspect', 'list-sources', 'discover', 'select-source', 'open-source', 'close-source']) {
|
||||
assert.ok(owner.commands.includes(cmd), `owner exposes ${cmd}`);
|
||||
}
|
||||
assert.equal(window.slopsmith.midiInput.version, 1);
|
||||
assert.equal(window.feedBack.midiInput.version, 1);
|
||||
});
|
||||
|
||||
test('list-sources and select-source are prompt-free (never enumerate)', async () => {
|
||||
const window = loadMidiInput();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
const calls = fakeProvider(window);
|
||||
const listed = await api.dispatch({ capability: 'midi-input', command: 'list-sources', source: 'tester' });
|
||||
assert.equal(listed.outcome, 'handled');
|
||||
@@ -57,12 +57,12 @@ test('list-sources and select-source are prompt-free (never enumerate)', async (
|
||||
|
||||
test('discover is the permission boundary and surfaces sources', async () => {
|
||||
const window = loadMidiInput();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
const calls = fakeProvider(window);
|
||||
const r = await api.dispatch({ capability: 'midi-input', command: 'discover', source: 'tester' });
|
||||
assert.equal(r.outcome, 'handled');
|
||||
assert.equal(calls.enumerate, 1, 'discover requests MIDI access exactly once');
|
||||
const sources = window.slopsmith.midiInput.listSources();
|
||||
const sources = window.feedBack.midiInput.listSources();
|
||||
assert.equal(sources.length, 1);
|
||||
assert.equal(sources[0].logicalSourceKey, 'web-midi::dev1');
|
||||
assert.equal(sources[0].kind, 'midi');
|
||||
@@ -71,31 +71,31 @@ test('discover is the permission boundary and surfaces sources', async () => {
|
||||
test('re-discovery drops sources for devices that vanished', async () => {
|
||||
const window = loadMidiInput();
|
||||
let devices = [{ sourceId: 'dev1', label: 'A' }, { sourceId: 'dev2', label: 'B' }];
|
||||
window.slopsmith.midiInput.registerProvider({
|
||||
window.feedBack.midiInput.registerProvider({
|
||||
providerId: 'web-midi', label: 'Web MIDI',
|
||||
enumerate: async () => devices,
|
||||
open: async () => ({ addListener() {}, removeListener() {} }),
|
||||
close: () => {},
|
||||
});
|
||||
await window.slopsmith.midiInput.discover();
|
||||
assert.equal(window.slopsmith.midiInput.listSources().length, 2);
|
||||
await window.feedBack.midiInput.discover();
|
||||
assert.equal(window.feedBack.midiInput.listSources().length, 2);
|
||||
devices = [{ sourceId: 'dev1', label: 'A' }]; // dev2 unplugged
|
||||
await window.slopsmith.midiInput.discover();
|
||||
const keys = window.slopsmith.midiInput.listSources().map((s) => s.logicalSourceKey);
|
||||
await window.feedBack.midiInput.discover();
|
||||
const keys = window.feedBack.midiInput.listSources().map((s) => s.logicalSourceKey);
|
||||
assert.equal(keys.length, 1, 'vanished device is dropped from the source list');
|
||||
assert.equal(keys[0], 'web-midi::dev1');
|
||||
});
|
||||
|
||||
test('discover with no provider reports unavailable', async () => {
|
||||
const window = loadMidiInput();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
const r = await api.dispatch({ capability: 'midi-input', command: 'discover', source: 'tester' });
|
||||
assert.equal(r.outcome, 'unavailable');
|
||||
});
|
||||
|
||||
test('discover surfaces denied when MIDI access is rejected', async () => {
|
||||
const window = loadMidiInput();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
fakeProvider(window, { handlers: { enumerate: async () => { throw new Error('SecurityError: permission denied'); } } });
|
||||
const r = await api.dispatch({ capability: 'midi-input', command: 'discover', source: 'tester' });
|
||||
assert.equal(r.outcome, 'denied');
|
||||
@@ -104,21 +104,21 @@ test('discover surfaces denied when MIDI access is rejected', async () => {
|
||||
|
||||
test('select-source persists by logicalSourceKey', async () => {
|
||||
const window = loadMidiInput();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
fakeProvider(window);
|
||||
await api.dispatch({ capability: 'midi-input', command: 'discover', source: 'tester' });
|
||||
const sel = await api.dispatch({ capability: 'midi-input', command: 'select-source', source: 'tester', payload: { logicalSourceKey: 'web-midi::dev1' } });
|
||||
assert.equal(sel.outcome, 'handled');
|
||||
assert.equal(window.__storage.get('slopsmith.midiInput.selectedLogicalSourceKey'), 'web-midi::dev1');
|
||||
assert.ok(window.slopsmith.midiInput.listSources()[0].selected);
|
||||
assert.equal(window.__storage.get('feedBack.midiInput.selectedLogicalSourceKey'), 'web-midi::dev1');
|
||||
assert.ok(window.feedBack.midiInput.listSources()[0].selected);
|
||||
});
|
||||
|
||||
test('open/close share one session and release on the last requester', async () => {
|
||||
const window = loadMidiInput();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
const calls = fakeProvider(window);
|
||||
await window.slopsmith.midiInput.discover();
|
||||
await window.slopsmith.midiInput.select('web-midi::dev1');
|
||||
await window.feedBack.midiInput.discover();
|
||||
await window.feedBack.midiInput.select('web-midi::dev1');
|
||||
const a = await api.dispatch({ capability: 'midi-input', command: 'open-source', source: 'reqA', payload: { logicalSourceKey: 'web-midi::dev1' } });
|
||||
const b = await api.dispatch({ capability: 'midi-input', command: 'open-source', source: 'reqB', payload: { logicalSourceKey: 'web-midi::dev1' } });
|
||||
assert.equal(a.outcome, 'handled');
|
||||
@@ -133,20 +133,20 @@ test('open/close share one session and release on the last requester', async ()
|
||||
|
||||
test('concurrent opens for one source coalesce onto a single provider.open', async () => {
|
||||
const window = loadMidiInput();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
// A provider whose open() stays pending until we release it, so both
|
||||
// dispatches are genuinely in flight at the same time.
|
||||
let release;
|
||||
const gate = new Promise((r) => { release = r; });
|
||||
const calls = { open: 0, close: 0 };
|
||||
window.slopsmith.midiInput.registerProvider({
|
||||
window.feedBack.midiInput.registerProvider({
|
||||
providerId: 'web-midi', label: 'Web MIDI',
|
||||
enumerate: async () => [{ sourceId: 'dev1', label: 'My Keyboard' }],
|
||||
open: async () => { calls.open += 1; await gate; return { addListener() {}, removeListener() {} }; },
|
||||
close: () => { calls.close += 1; },
|
||||
});
|
||||
await window.slopsmith.midiInput.discover();
|
||||
await window.slopsmith.midiInput.select('web-midi::dev1');
|
||||
await window.feedBack.midiInput.discover();
|
||||
await window.feedBack.midiInput.select('web-midi::dev1');
|
||||
const p1 = api.dispatch({ capability: 'midi-input', command: 'open-source', source: 'reqA', payload: { logicalSourceKey: 'web-midi::dev1' } });
|
||||
const p2 = api.dispatch({ capability: 'midi-input', command: 'open-source', source: 'reqB', payload: { logicalSourceKey: 'web-midi::dev1' } });
|
||||
release();
|
||||
@@ -165,9 +165,9 @@ test('concurrent opens for one source coalesce onto a single provider.open', asy
|
||||
test('public open() surfaces the live handle (in-page only)', async () => {
|
||||
const window = loadMidiInput();
|
||||
fakeProvider(window);
|
||||
await window.slopsmith.midiInput.discover();
|
||||
await window.slopsmith.midiInput.select('web-midi::dev1');
|
||||
const res = await window.slopsmith.midiInput.open({ requester: 'input_setup', logicalSourceKey: 'web-midi::dev1' });
|
||||
await window.feedBack.midiInput.discover();
|
||||
await window.feedBack.midiInput.select('web-midi::dev1');
|
||||
const res = await window.feedBack.midiInput.open({ requester: 'input_setup', logicalSourceKey: 'web-midi::dev1' });
|
||||
assert.equal(res.outcome, 'handled');
|
||||
assert.ok(res.handle && typeof res.handle.addListener === 'function', 'live handle exposed via public global');
|
||||
});
|
||||
@@ -193,12 +193,12 @@ test('built-in Web-MIDI provider self-registers + discovers, filtering loopback
|
||||
{ id: 'kb1', name: 'My Keyboard' },
|
||||
{ id: 'thru', name: 'Midi Through Port-0' }, // loopback → filtered out
|
||||
]);
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
assert.ok(api.inspect('midi-input').participants.some(p => p.pluginId === 'core.midi-input'),
|
||||
'built-in provider registered without any plugin');
|
||||
const r = await api.dispatch({ capability: 'midi-input', command: 'discover', source: 'tester' });
|
||||
assert.equal(r.outcome, 'handled');
|
||||
const sources = window.slopsmith.midiInput.listSources();
|
||||
const sources = window.feedBack.midiInput.listSources();
|
||||
assert.equal(sources.length, 1, 'loopback/passthrough ports are filtered');
|
||||
assert.equal(sources[0].logicalSourceKey, 'web-midi::kb1');
|
||||
});
|
||||
@@ -206,10 +206,10 @@ test('built-in Web-MIDI provider self-registers + discovers, filtering loopback
|
||||
test('diagnostics are redaction-safe (no device labels, no raw messages)', async () => {
|
||||
const window = loadMidiInput();
|
||||
fakeProvider(window);
|
||||
await window.slopsmith.midiInput.discover();
|
||||
const contrib = window.slopsmith.diagnostics.snapshotContributions()['midi-input-capability'];
|
||||
await window.feedBack.midiInput.discover();
|
||||
const contrib = window.feedBack.diagnostics.snapshotContributions()['midi-input-capability'];
|
||||
assert.ok(contrib, 'midi-input contributes diagnostics');
|
||||
assert.equal(contrib.schema, 'slopsmith.midi_input.diagnostics.v1');
|
||||
assert.equal(contrib.schema, 'feedBack.midi_input.diagnostics.v1');
|
||||
const serialized = JSON.stringify(contrib);
|
||||
assert.ok(!serialized.includes('My Keyboard'), 'device labels are redacted from diagnostics');
|
||||
for (const s of contrib.sources) assert.ok(!('label' in s), 'source entries carry no label');
|
||||
|
||||
@@ -34,19 +34,19 @@ async function registerMidiProvider(api) {
|
||||
|
||||
test('note-detection domain registers an active sensitive provider-coordinator', () => {
|
||||
const window = loadNoteDetection();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
const pipeline = api.inspect('note-detection');
|
||||
assert.ok(pipeline, 'note-detection pipeline exists');
|
||||
const owner = (pipeline.participants || []).find(p => p.pluginId === 'core.note-detection');
|
||||
assert.ok(owner, 'core.note-detection owner registered');
|
||||
assert.equal(owner.safety, 'sensitive');
|
||||
assert.ok(owner.commands.includes('open-binding'));
|
||||
assert.equal(window.slopsmith.noteDetection.version, 1);
|
||||
assert.equal(window.feedBack.noteDetection.version, 1);
|
||||
});
|
||||
|
||||
test('open-binding without a provider reports unavailable, never a silent verdict', async () => {
|
||||
const window = loadNoteDetection();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
const result = await api.dispatch({
|
||||
capability: 'note-detection', command: 'open-binding',
|
||||
source: 'keys_highway_3d', payload: { context: { arrangement: 'keys' } },
|
||||
@@ -57,7 +57,7 @@ test('open-binding without a provider reports unavailable, never a silent verdic
|
||||
|
||||
test('provider registration + binding lifecycle with per-binding context', async () => {
|
||||
const window = loadNoteDetection();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
const events = captureEvents(api, [
|
||||
'note-detection:provider-registered',
|
||||
'note-detection:binding-opened',
|
||||
@@ -104,7 +104,7 @@ test('provider registration + binding lifecycle with per-binding context', async
|
||||
|
||||
test('concurrent bindings keep independent contexts (FR-003)', async () => {
|
||||
const window = loadNoteDetection();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
await registerMidiProvider(api);
|
||||
const a = await api.dispatch({
|
||||
capability: 'note-detection', command: 'open-binding',
|
||||
@@ -114,7 +114,7 @@ test('concurrent bindings keep independent contexts (FR-003)', async () => {
|
||||
capability: 'note-detection', command: 'open-binding',
|
||||
source: 'slopscale', payload: { context: { arrangement: 'bass', stringCount: 4, capo: 0 } },
|
||||
});
|
||||
const snapshot = window.slopsmith.noteDetection.snapshot();
|
||||
const snapshot = window.feedBack.noteDetection.snapshot();
|
||||
const ctxA = snapshot.bindings.find(x => x.id === a.payload.bindingId).context;
|
||||
const ctxB = snapshot.bindings.find(x => x.id === b.payload.bindingId).context;
|
||||
assert.equal(ctxA.arrangement, 'guitar');
|
||||
@@ -125,18 +125,18 @@ test('concurrent bindings keep independent contexts (FR-003)', async () => {
|
||||
|
||||
test('unregistering a provider closes its bindings and flips availability', async () => {
|
||||
const window = loadNoteDetection();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
const events = captureEvents(api, ['note-detection:availability-changed', 'note-detection:binding-closed']);
|
||||
await registerMidiProvider(api);
|
||||
const openResult = await api.dispatch({ capability: 'note-detection', command: 'open-binding', source: 'keys_highway_3d', payload: {} });
|
||||
assert.equal(openResult.outcome, 'handled');
|
||||
assert.equal(window.slopsmith.noteDetection.snapshot().bindings.length, 1);
|
||||
assert.equal(window.feedBack.noteDetection.snapshot().bindings.length, 1);
|
||||
const result = await api.dispatch({
|
||||
capability: 'note-detection', command: 'unregister-provider',
|
||||
source: 'keys_highway_3d', payload: { providerId: 'keys-midi' },
|
||||
});
|
||||
assert.equal(result.outcome, 'handled');
|
||||
assert.equal(window.slopsmith.noteDetection.snapshot().bindings.length, 0);
|
||||
assert.equal(window.feedBack.noteDetection.snapshot().bindings.length, 0);
|
||||
const availability = events.filter(e => e.event === 'availability-changed').map(e => e.payload.available);
|
||||
assert.deepEqual(JSON.parse(JSON.stringify(availability)), [true, false]);
|
||||
assert.ok(events.some(e => e.event === 'binding-closed' && e.payload.reason === 'provider-unregistered'));
|
||||
@@ -149,10 +149,10 @@ test('unregistering a provider closes its bindings and flips availability', asyn
|
||||
|
||||
test('hit/miss reports flow as observability events with bounded fields', () => {
|
||||
const window = loadNoteDetection();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
const events = captureEvents(api, ['note-detection:hit', 'note-detection:miss']);
|
||||
window.slopsmith.noteDetection.reportHit({ bindingId: 'ndb-1', providerId: 'keys-midi', midi: 64, hit: true, secretDevice: 'Yamaha P-125' });
|
||||
window.slopsmith.noteDetection.reportMiss({ bindingId: 'ndb-1', providerId: 'keys-midi', midi: 65, hit: false });
|
||||
window.feedBack.noteDetection.reportHit({ bindingId: 'ndb-1', providerId: 'keys-midi', midi: 64, hit: true, secretDevice: 'Yamaha P-125' });
|
||||
window.feedBack.noteDetection.reportMiss({ bindingId: 'ndb-1', providerId: 'keys-midi', midi: 65, hit: false });
|
||||
assert.equal(events.length, 2);
|
||||
assert.equal(events[0].payload.midi, 64);
|
||||
// Unknown fields are dropped — payloads stay bounded and device-label free.
|
||||
@@ -162,27 +162,27 @@ test('hit/miss reports flow as observability events with bounded fields', () =>
|
||||
|
||||
test('diagnostics contribution is redaction-safe', async () => {
|
||||
const window = loadNoteDetection();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
await registerMidiProvider(api);
|
||||
await api.dispatch({
|
||||
capability: 'note-detection', command: 'open-binding',
|
||||
source: 'keys_highway_3d', payload: { context: { arrangement: 'keys', deviceLabel: 'Yamaha P-125' } },
|
||||
});
|
||||
window.slopsmith.noteDetection.reportHit({ bindingId: 'ndb-1', midi: 60, hit: true });
|
||||
window.feedBack.noteDetection.reportHit({ bindingId: 'ndb-1', midi: 60, hit: true });
|
||||
const contribution = window.__diagnosticsContributions.get('note-detection-capability');
|
||||
assert.equal(contribution.schema, 'slopsmith.note_detection_capability.v1');
|
||||
assert.equal(contribution.schema, 'feedBack.note_detection_capability.v1');
|
||||
const serialized = JSON.stringify(contribution);
|
||||
assert.ok(!/Yamaha|deviceLabel|filename|\.sloppak|\.archive/i.test(serialized), serialized);
|
||||
});
|
||||
|
||||
test('legacy setNoteStateProvider surface is wrapped and accounted', () => {
|
||||
const window = loadNoteDetection();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
// Simulate highway.js arriving after the host, then the notedetect
|
||||
// plugin installing its chart-coupled provider.
|
||||
let installed = null;
|
||||
window.highway = { setNoteStateProvider(fn) { installed = fn; } };
|
||||
window.slopsmith.emit('song:loaded', {});
|
||||
window.feedBack.emit('song:loaded', {});
|
||||
const provider = () => ({ state: 'hit' });
|
||||
window.highway.setNoteStateProvider(provider);
|
||||
assert.equal(installed, provider, 'legacy behavior preserved');
|
||||
@@ -196,7 +196,7 @@ test('legacy setNoteStateProvider surface is wrapped and accounted', () => {
|
||||
|
||||
test('unsupported binding/provider ids degrade with bounded reasons (FR-008)', async () => {
|
||||
const window = loadNoteDetection();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
await registerMidiProvider(api);
|
||||
const badProvider = await api.dispatch({
|
||||
capability: 'note-detection', command: 'open-binding',
|
||||
@@ -212,7 +212,7 @@ test('unsupported binding/provider ids degrade with bounded reasons (FR-008)', a
|
||||
|
||||
test('_contextSummary whitelists arrangement kind — unknown values are dropped', async () => {
|
||||
const window = loadNoteDetection();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
await registerMidiProvider(api);
|
||||
// Known arrangement kinds pass through.
|
||||
const open = await api.dispatch({
|
||||
@@ -220,7 +220,7 @@ test('_contextSummary whitelists arrangement kind — unknown values are dropped
|
||||
source: 'caller', payload: { context: { arrangement: 'keys', stringCount: 6 } },
|
||||
});
|
||||
assert.equal(open.outcome, 'handled');
|
||||
const snap = window.slopsmith.noteDetection.snapshot();
|
||||
const snap = window.feedBack.noteDetection.snapshot();
|
||||
const ctx = snap.bindings.find(b => b.id === open.payload.bindingId).context;
|
||||
assert.equal(ctx.arrangement, 'keys');
|
||||
|
||||
@@ -230,28 +230,28 @@ test('_contextSummary whitelists arrangement kind — unknown values are dropped
|
||||
source: 'caller', payload: { context: { arrangement: '/Users/victim/song.archive' } },
|
||||
});
|
||||
assert.equal(open2.outcome, 'handled');
|
||||
const snap2 = window.slopsmith.noteDetection.snapshot();
|
||||
const snap2 = window.feedBack.noteDetection.snapshot();
|
||||
const ctx2 = snap2.bindings.find(b => b.id === open2.payload.bindingId).context;
|
||||
assert.equal(ctx2.arrangement, undefined, 'path-bearing arrangement must be dropped');
|
||||
});
|
||||
|
||||
test('snapshot primitives are deep-copied — caller cannot mutate provider internals', async () => {
|
||||
const window = loadNoteDetection();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
await registerMidiProvider(api);
|
||||
const snap1 = window.slopsmith.noteDetection.snapshot();
|
||||
const snap1 = window.feedBack.noteDetection.snapshot();
|
||||
const providerEntry = snap1.providers.find(p => p.id === 'keys-midi');
|
||||
assert.ok(Array.isArray(providerEntry.primitives));
|
||||
// Mutate the copy — must not affect subsequent snapshots.
|
||||
providerEntry.primitives.push('injected');
|
||||
const snap2 = window.slopsmith.noteDetection.snapshot();
|
||||
const snap2 = window.feedBack.noteDetection.snapshot();
|
||||
const providerEntry2 = snap2.providers.find(p => p.id === 'keys-midi');
|
||||
assert.ok(!providerEntry2.primitives.includes('injected'), 'live primitives must not be mutated via snapshot');
|
||||
});
|
||||
|
||||
test('close-binding and set-target enforce requester ownership', async () => {
|
||||
const window = loadNoteDetection();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
await registerMidiProvider(api);
|
||||
const open = await api.dispatch({
|
||||
capability: 'note-detection', command: 'open-binding',
|
||||
@@ -283,7 +283,7 @@ test('close-binding and set-target enforce requester ownership', async () => {
|
||||
|
||||
test('register-provider rejects cross-owner re-registration', async () => {
|
||||
const window = loadNoteDetection();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
// First registration by plugin-a.
|
||||
const first = await api.dispatch({
|
||||
capability: 'note-detection', command: 'register-provider',
|
||||
@@ -311,7 +311,7 @@ test('register-provider rejects cross-owner re-registration', async () => {
|
||||
|
||||
test('unregister-provider rejects cross-owner unregister', async () => {
|
||||
const window = loadNoteDetection();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
await registerMidiProvider(api);
|
||||
|
||||
// A different participant must not unregister a provider it does not own.
|
||||
@@ -322,7 +322,7 @@ test('unregister-provider rejects cross-owner unregister', async () => {
|
||||
assert.equal(steal.outcome, 'degraded', 'cross-owner unregister must be rejected');
|
||||
|
||||
// Provider must still be present after the failed cross-owner attempt.
|
||||
const snap = window.slopsmith.noteDetection.snapshot();
|
||||
const snap = window.feedBack.noteDetection.snapshot();
|
||||
assert.ok(snap.providers.some(p => p.id === 'keys-midi'), 'provider must survive cross-owner unregister attempt');
|
||||
|
||||
// The original owner can still unregister.
|
||||
@@ -335,7 +335,7 @@ test('unregister-provider rejects cross-owner unregister', async () => {
|
||||
|
||||
test('availability-changed fires only on 0→1 and 1→0 transitions', async () => {
|
||||
const window = loadNoteDetection();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
const events = captureEvents(api, ['note-detection:availability-changed']);
|
||||
|
||||
// First registration: 0→1, should emit.
|
||||
@@ -364,7 +364,7 @@ test('availability-changed fires only on 0→1 and 1→0 transitions', async ()
|
||||
|
||||
test('unregistering one of two providers from the same participant keeps participant in the pipeline', async () => {
|
||||
const window = loadNoteDetection();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
|
||||
// One plugin registers two providers under the same participantId.
|
||||
await api.dispatch({
|
||||
@@ -386,7 +386,7 @@ test('unregistering one of two providers from the same participant keeps partici
|
||||
});
|
||||
assert.equal(unreg.outcome, 'handled');
|
||||
|
||||
const snap = window.slopsmith.noteDetection.snapshot();
|
||||
const snap = window.feedBack.noteDetection.snapshot();
|
||||
assert.ok(!snap.providers.some(p => p.id === 'multi-provider-a'), 'provider-a must be removed');
|
||||
assert.ok(snap.providers.some(p => p.id === 'multi-provider-b'), 'provider-b must still be present');
|
||||
const participants = api.inspect('note-detection').participants || [];
|
||||
|
||||
@@ -21,7 +21,7 @@ function loadCables(opts) {
|
||||
opts = opts || {};
|
||||
const rafCalls = [];
|
||||
const created = []; // every createElementNS node
|
||||
const win = { slopsmith: null };
|
||||
const win = { feedBack: null };
|
||||
win.addEventListener = () => {};
|
||||
win.matchMedia = () => ({ matches: !!opts.reduce });
|
||||
const doc = {
|
||||
|
||||
@@ -26,7 +26,7 @@ function buildReadySandbox() {
|
||||
const listeners = new Map();
|
||||
const sandbox = {
|
||||
window: {
|
||||
slopsmith: {
|
||||
feedBack: {
|
||||
on(event, fn) { listeners.set(event, fn); },
|
||||
off(event, fn) { if (listeners.get(event) === fn) listeners.delete(event); },
|
||||
},
|
||||
@@ -84,5 +84,5 @@ test('playback adapter suppresses duplicate HTML5 pause events before emitting c
|
||||
const src = fs.readFileSync(APP_JS, 'utf8');
|
||||
const fn = extractFunction(src, 'function _installPlaybackTransportAdapter()');
|
||||
|
||||
assert.match(fn, /if \(!window\._juceMode && wasPlaying\) \{\s*isPlaying = false;\s*window\.slopsmith\.isPlaying = false;\s*audio\.pause\(\);\s*_markPlaybackPaused\(\);\s*\}/);
|
||||
assert.match(fn, /if \(!window\._juceMode && wasPlaying\) \{\s*isPlaying = false;\s*window\.feedBack\.isPlaying = false;\s*audio\.pause\(\);\s*_markPlaybackPaused\(\);\s*\}/);
|
||||
});
|
||||
|
||||
@@ -25,14 +25,14 @@ test('plugin fresh starts require user action and incompatible playback particip
|
||||
const fixture = JSON.parse(fs.readFileSync(path.join(ROOT, 'tests', 'fixtures', 'plugin_capabilities', 'unsupported_capability_version.json'), 'utf8'));
|
||||
fixture.id = 'future_playback';
|
||||
fixture.capabilities = { playback: { roles: ['owner'], commands: ['inspect'], runtime: true, version: 999, handlers: { inspect: () => ({ outcome: 'handled' }) } } };
|
||||
incompatibleWindow.slopsmith.capabilities.registerParticipants([fixture]);
|
||||
const result = await incompatibleWindow.slopsmith.capabilities.dispatch({ capability: 'playback', command: 'inspect', requester: 'test' });
|
||||
incompatibleWindow.feedBack.capabilities.registerParticipants([fixture]);
|
||||
const result = await incompatibleWindow.feedBack.capabilities.dispatch({ capability: 'playback', command: 'inspect', requester: 'test' });
|
||||
assert.equal(result.status, 'incompatible-version');
|
||||
});
|
||||
|
||||
test('same-priority latest controls remain non-stale while user-priority commands deny background automation', async () => {
|
||||
const window = loadPlayback();
|
||||
window.slopsmith.playback.registerTransportAdapter(makeAdapter());
|
||||
window.feedBack.playback.registerTransportAdapter(makeAdapter());
|
||||
await dispatch(window, 'start', { authorization: 'user-action', requesterId: 'core.player.controls', target: makeTarget() });
|
||||
const firstPause = await dispatch(window, 'pause', { requesterId: 'plugin.a', priority: 'normal' });
|
||||
const normalResume = await dispatch(window, 'resume', { requesterId: 'plugin.b', priority: 'normal' });
|
||||
@@ -48,7 +48,7 @@ test('legacy bridge hits are attributed to playback compatibility shims', () =>
|
||||
const window = loadPlayback();
|
||||
const bridgeEvents = captureEvents(window, 'playback:bridge-hit');
|
||||
|
||||
window.slopsmith.playback.recordBridgeHit({
|
||||
window.feedBack.playback.recordBridgeHit({
|
||||
bridgeId: 'playback.window-play-song',
|
||||
legacySurface: 'window.playSong',
|
||||
source: 'core.app',
|
||||
@@ -56,7 +56,7 @@ test('legacy bridge hits are attributed to playback compatibility shims', () =>
|
||||
});
|
||||
|
||||
const playback = diagnosticsSnapshot(window);
|
||||
const runtime = window.slopsmith.capabilities.snapshotDiagnostics();
|
||||
const runtime = window.feedBack.capabilities.snapshotDiagnostics();
|
||||
const shim = runtime.compatibilityShims.find(item => item.capability === 'playback' && item.legacySurface === 'window.playSong');
|
||||
|
||||
assert.equal(bridgeEvents.length, 1);
|
||||
@@ -68,10 +68,10 @@ test('legacy bridge hits are attributed to playback compatibility shims', () =>
|
||||
|
||||
test('legacy song events update playback state without exposing raw filenames', () => {
|
||||
const window = loadPlayback();
|
||||
window.slopsmith.emit('song:loading', { filename: '/Users/example/Secret Folder/Artist - Song_p.archive', arrangement: 0 });
|
||||
window.slopsmith.emit('song:loaded', makeTarget({ filename: '/Users/example/Secret Folder/Artist - Song_p.archive' }));
|
||||
window.slopsmith.emit('song:play', { time: 4, audioT: 4, chartT: 4 });
|
||||
window.slopsmith.emit('song:seek', { from: 4, to: 12, reason: 'seek-by' });
|
||||
window.feedBack.emit('song:loading', { filename: '/Users/example/Secret Folder/Artist - Song_p.archive', arrangement: 0 });
|
||||
window.feedBack.emit('song:loaded', makeTarget({ filename: '/Users/example/Secret Folder/Artist - Song_p.archive' }));
|
||||
window.feedBack.emit('song:play', { time: 4, audioT: 4, chartT: 4 });
|
||||
window.feedBack.emit('song:seek', { from: 4, to: 12, reason: 'seek-by' });
|
||||
|
||||
const playback = diagnosticsSnapshot(window);
|
||||
const encoded = JSON.stringify(playback);
|
||||
@@ -89,8 +89,8 @@ test('route changes are captured as redaction-safe playback lifecycle events', (
|
||||
const changing = captureEvents(window, 'playback:route-changing');
|
||||
const changed = captureEvents(window, 'playback:route-changed');
|
||||
|
||||
window.slopsmith.playback.recordRouteChange({ routeKind: 'desktop-native', state: 'switching', preservedTime: true, safeReason: 'desktop engine active' });
|
||||
window.slopsmith.playback.recordRouteChange({ routeKind: 'desktop-native', state: 'active', preservedTime: true, safeReason: 'desktop route active' });
|
||||
window.feedBack.playback.recordRouteChange({ routeKind: 'desktop-native', state: 'switching', preservedTime: true, safeReason: 'desktop engine active' });
|
||||
window.feedBack.playback.recordRouteChange({ routeKind: 'desktop-native', state: 'active', preservedTime: true, safeReason: 'desktop route active' });
|
||||
|
||||
const playback = diagnosticsSnapshot(window);
|
||||
assert.equal(changing.length, 1);
|
||||
|
||||
@@ -4,7 +4,7 @@ const { loadPlayback, captureEvents, dispatch, diagnosticsSnapshot, makeTarget,
|
||||
|
||||
test('exported diagnostics pseudonymize targets while local inspector may show display names', async () => {
|
||||
const window = loadPlayback();
|
||||
window.slopsmith.playback.registerTransportAdapter(makeAdapter());
|
||||
window.feedBack.playback.registerTransportAdapter(makeAdapter());
|
||||
await dispatch(window, 'start', {
|
||||
authorization: 'user-action',
|
||||
requesterId: 'core.player.controls',
|
||||
@@ -33,7 +33,7 @@ test('exported diagnostics pseudonymize targets while local inspector may show d
|
||||
|
||||
test('diagnostic history is bounded for current and stopped sessions', async () => {
|
||||
const window = loadPlayback();
|
||||
window.slopsmith.playback.registerTransportAdapter(makeAdapter());
|
||||
window.feedBack.playback.registerTransportAdapter(makeAdapter());
|
||||
|
||||
for (let index = 0; index < 7; index += 1) {
|
||||
await dispatch(window, 'start', { authorization: 'user-action', requesterId: 'core.player.controls', target: makeTarget({ filename: `song-${index}.archive`, title: `Song ${index}` }) });
|
||||
@@ -55,11 +55,11 @@ test('diagnostic history is bounded for current and stopped sessions', async ()
|
||||
|
||||
test('diagnostics contribution is exported under playback schema', async () => {
|
||||
const window = loadPlayback();
|
||||
window.slopsmith.playback.registerTransportAdapter(makeAdapter());
|
||||
window.feedBack.playback.registerTransportAdapter(makeAdapter());
|
||||
await dispatch(window, 'start', { authorization: 'user-action', requesterId: 'core.player.controls', target: makeTarget() });
|
||||
|
||||
const contribution = window.slopsmith.diagnostics.snapshotContributions().playback;
|
||||
assert.equal(contribution.schema, 'slopsmith.playback.diagnostics.v1');
|
||||
const contribution = window.feedBack.diagnostics.snapshotContributions().playback;
|
||||
assert.equal(contribution.schema, 'feedBack.playback.diagnostics.v1');
|
||||
assert.equal(contribution.domain, 'playback');
|
||||
assert.equal(contribution.exportMode, 'exported');
|
||||
assert.match(contribution.state.target.settingsKey, /^settings-v1-[a-z0-9]{7}$/);
|
||||
@@ -68,11 +68,11 @@ test('diagnostics contribution is exported under playback schema', async () => {
|
||||
|
||||
test('diagnostics redact caller-supplied route and stale session ids', async () => {
|
||||
const window = loadPlayback();
|
||||
window.slopsmith.playback.registerTransportAdapter(makeAdapter());
|
||||
window.feedBack.playback.registerTransportAdapter(makeAdapter());
|
||||
await dispatch(window, 'start', { authorization: 'user-action', requesterId: 'core.player.controls', target: makeTarget() });
|
||||
|
||||
await dispatch(window, 'pause', { sessionId: '/Users/example/private-session?token=secret' }, 'plugin.remote');
|
||||
window.slopsmith.playback.recordRouteChange({ routeId: '/Users/example/native-route?token=secret', routeKind: 'desktop-native', state: 'active', safeReason: 'ok' });
|
||||
window.feedBack.playback.recordRouteChange({ routeId: '/Users/example/native-route?token=secret', routeKind: 'desktop-native', state: 'active', safeReason: 'ok' });
|
||||
|
||||
const snapshot = diagnosticsSnapshot(window);
|
||||
const encoded = JSON.stringify(snapshot);
|
||||
@@ -87,11 +87,11 @@ test('diagnostics redact caller-supplied route and stale session ids', async ()
|
||||
|
||||
test('diagnostics redact requester ids and raw camel-case payload keys', async () => {
|
||||
const window = loadPlayback();
|
||||
window.slopsmith.playback.registerTransportAdapter(makeAdapter());
|
||||
window.feedBack.playback.registerTransportAdapter(makeAdapter());
|
||||
await dispatch(window, 'start', { authorization: 'user-action', target: makeTarget() }, '/Users/example/plugin token=secret');
|
||||
|
||||
const degradedEvents = captureEvents(window, 'playback:degraded');
|
||||
window.slopsmith.playback.transportEvent('degraded', {
|
||||
window.feedBack.playback.transportEvent('degraded', {
|
||||
requesterId: '/Users/example/transport token=secret',
|
||||
accessToken: 'plain-secret-token',
|
||||
nativeHandleRef: 'native-secret-handle',
|
||||
@@ -99,7 +99,7 @@ test('diagnostics redact requester ids and raw camel-case payload keys', async (
|
||||
reason: '/Users/example/private song.archive token=secret',
|
||||
safeDetail: 'safe value',
|
||||
});
|
||||
window.slopsmith.playback.recordBridgeHit({
|
||||
window.feedBack.playback.recordBridgeHit({
|
||||
bridgeId: '/Users/example/bridge token=secret',
|
||||
legacySurface: 'window.playSong',
|
||||
source: '/Users/example/source token=secret',
|
||||
|
||||
@@ -5,12 +5,12 @@ const { loadCapabilities } = require('./capabilities_test_harness');
|
||||
|
||||
test('playback reports no-owner no-handler and unsupported command outcomes explicitly', async () => {
|
||||
const noOwnerWindow = loadCapabilities();
|
||||
const noOwner = await noOwnerWindow.slopsmith.capabilities.dispatch({ capability: 'playback', command: 'inspect', requester: 'test' });
|
||||
const noOwner = await noOwnerWindow.feedBack.capabilities.dispatch({ capability: 'playback', command: 'inspect', requester: 'test' });
|
||||
assert.equal(noOwner.status, 'no-owner');
|
||||
|
||||
const noHandlerWindow = loadCapabilities();
|
||||
noHandlerWindow.slopsmith.capabilities.registerOwner('playback', { pluginId: 'test-owner', commands: ['inspect'], events: [] });
|
||||
const noHandler = await noHandlerWindow.slopsmith.capabilities.dispatch({ capability: 'playback', command: 'inspect', requester: 'test' });
|
||||
noHandlerWindow.feedBack.capabilities.registerOwner('playback', { pluginId: 'test-owner', commands: ['inspect'], events: [] });
|
||||
const noHandler = await noHandlerWindow.feedBack.capabilities.dispatch({ capability: 'playback', command: 'inspect', requester: 'test' });
|
||||
assert.equal(noHandler.status, 'no-handler');
|
||||
|
||||
const unsupportedWindow = loadPlayback();
|
||||
@@ -20,7 +20,7 @@ test('playback reports no-owner no-handler and unsupported command outcomes expl
|
||||
|
||||
test('playback registers as an active core owner', async () => {
|
||||
const window = loadPlayback();
|
||||
const snapshot = window.slopsmith.capabilities.snapshotDiagnostics();
|
||||
const snapshot = window.feedBack.capabilities.snapshotDiagnostics();
|
||||
const playback = snapshot.pipelines.find(pipeline => pipeline.name === 'playback');
|
||||
|
||||
assert.ok(playback, 'playback pipeline exists');
|
||||
@@ -40,7 +40,7 @@ test('start requires a target and explicit user authorization for fresh audible
|
||||
assert.equal(noGesture.status, 'user-action-required');
|
||||
|
||||
const adapter = makeAdapter();
|
||||
window.slopsmith.playback.registerTransportAdapter(adapter);
|
||||
window.feedBack.playback.registerTransportAdapter(adapter);
|
||||
const events = captureEvents(window, 'playback:ready');
|
||||
const result = await dispatch(window, 'start', { requesterId: 'core.player.controls', authorization: 'user-action', target: makeTarget() });
|
||||
|
||||
@@ -56,7 +56,7 @@ test('start requires a target and explicit user authorization for fresh audible
|
||||
|
||||
test('settings key is stable across arrangements while target id remains arrangement scoped', async () => {
|
||||
const window = loadPlayback();
|
||||
window.slopsmith.playback.registerTransportAdapter(makeAdapter());
|
||||
window.feedBack.playback.registerTransportAdapter(makeAdapter());
|
||||
const base = makeTarget({ filename: '/Users/example/DLC/Artist - Song_p.archive', arrangement: 'Lead', arrangementIndex: 0 });
|
||||
|
||||
await dispatch(window, 'start', { requesterId: 'core.player.controls', authorization: 'user-action', target: base });
|
||||
@@ -71,7 +71,7 @@ test('settings key is stable across arrangements while target id remains arrange
|
||||
|
||||
test('unsafe caller-supplied settings keys are hashed before exposure', async () => {
|
||||
const window = loadPlayback();
|
||||
window.slopsmith.playback.registerTransportAdapter(makeAdapter());
|
||||
window.feedBack.playback.registerTransportAdapter(makeAdapter());
|
||||
|
||||
await dispatch(window, 'start', {
|
||||
requesterId: 'core.player.controls',
|
||||
@@ -86,7 +86,7 @@ test('unsafe caller-supplied settings keys are hashed before exposure', async ()
|
||||
|
||||
test('unsafe caller-supplied target ids are hashed before exposure', async () => {
|
||||
const window = loadPlayback();
|
||||
window.slopsmith.playback.registerTransportAdapter(makeAdapter());
|
||||
window.feedBack.playback.registerTransportAdapter(makeAdapter());
|
||||
|
||||
await dispatch(window, 'start', {
|
||||
requesterId: 'core.player.controls',
|
||||
@@ -101,7 +101,7 @@ test('unsafe caller-supplied target ids are hashed before exposure', async () =>
|
||||
|
||||
test('dispatch requester owns playback command attribution', async () => {
|
||||
const window = loadPlayback();
|
||||
window.slopsmith.playback.registerTransportAdapter(makeAdapter());
|
||||
window.feedBack.playback.registerTransportAdapter(makeAdapter());
|
||||
|
||||
const started = await dispatch(window, 'start', { requesterId: 'core.player.controls', authorization: 'user-action', target: makeTarget() }, 'plugin.remote');
|
||||
const paused = await dispatch(window, 'pause', { requesterId: 'core.player.controls' }, 'plugin.remote');
|
||||
@@ -118,10 +118,10 @@ test('dispatch requester owns playback command attribution', async () => {
|
||||
test('transport commands emit ordered lifecycle events and normalize outcomes', async () => {
|
||||
const window = loadPlayback();
|
||||
const adapter = makeAdapter({ duration: 10 });
|
||||
window.slopsmith.playback.registerTransportAdapter(adapter);
|
||||
window.feedBack.playback.registerTransportAdapter(adapter);
|
||||
const events = [];
|
||||
for (const eventName of ['playback:requested', 'playback:loading', 'playback:ready', 'playback:paused', 'playback:resumed', 'playback:seeking', 'playback:seeked', 'playback:loop-set', 'playback:loop-cleared']) {
|
||||
window.slopsmith.on(eventName, event => events.push(event.type.replace('playback:', '')));
|
||||
window.feedBack.on(eventName, event => events.push(event.type.replace('playback:', '')));
|
||||
}
|
||||
|
||||
await dispatch(window, 'start', { authorization: 'user-action', requesterId: 'core.player.controls', target: makeTarget() });
|
||||
@@ -141,22 +141,22 @@ test('transport commands emit ordered lifecycle events and normalize outcomes',
|
||||
|
||||
test('seek preserves pre-seek playback state and external seek events update state', async () => {
|
||||
const window = loadPlayback();
|
||||
window.slopsmith.playback.registerTransportAdapter(makeAdapter({ startPlaying: true, seekResult: { completed: true, from: 1, to: 5 } }));
|
||||
window.feedBack.playback.registerTransportAdapter(makeAdapter({ startPlaying: true, seekResult: { completed: true, from: 1, to: 5 } }));
|
||||
await dispatch(window, 'start', { authorization: 'user-action', requesterId: 'core.player.controls', target: makeTarget() });
|
||||
|
||||
const seek = await dispatch(window, 'seek', { requesterId: 'core.player.controls', time: 5 });
|
||||
assert.equal(seek.status, 'completed');
|
||||
assert.equal(diagnosticsSnapshot(window).state.state, 'playing');
|
||||
|
||||
window.slopsmith.playback.transportEvent('seeking', { requesterId: 'core.player.controls', media: { currentTime: 5 }, isPlaying: true });
|
||||
window.feedBack.playback.transportEvent('seeking', { requesterId: 'core.player.controls', media: { currentTime: 5 }, isPlaying: true });
|
||||
assert.equal(diagnosticsSnapshot(window).state.state, 'seeking');
|
||||
window.slopsmith.playback.transportEvent('seeked', { requesterId: 'core.player.controls', media: { currentTime: 9 }, isPlaying: true });
|
||||
window.feedBack.playback.transportEvent('seeked', { requesterId: 'core.player.controls', media: { currentTime: 9 }, isPlaying: true });
|
||||
assert.equal(diagnosticsSnapshot(window).state.state, 'playing');
|
||||
});
|
||||
|
||||
test('clear-loop requires an active playback session', async () => {
|
||||
const window = loadPlayback();
|
||||
window.slopsmith.playback.registerTransportAdapter(makeAdapter());
|
||||
window.feedBack.playback.registerTransportAdapter(makeAdapter());
|
||||
|
||||
const cleared = await dispatch(window, 'clear-loop', { requesterId: 'core.player.controls' });
|
||||
assert.equal(cleared.status, 'no-target');
|
||||
@@ -164,13 +164,13 @@ test('clear-loop requires an active playback session', async () => {
|
||||
|
||||
test('ended transport events and seek failure/rollback outcomes are distinguishable', async () => {
|
||||
const window = loadPlayback();
|
||||
window.slopsmith.playback.registerTransportAdapter(makeAdapter({ seekResult: { completed: true, from: 5, to: 4.5 } }));
|
||||
window.feedBack.playback.registerTransportAdapter(makeAdapter({ seekResult: { completed: true, from: 5, to: 4.5 } }));
|
||||
await dispatch(window, 'start', { authorization: 'user-action', requesterId: 'core.player.controls', target: makeTarget() });
|
||||
const rolledBack = await dispatch(window, 'seek', { requesterId: 'core.player.controls', time: 8 });
|
||||
assert.equal(rolledBack.status, 'rolled-back');
|
||||
|
||||
const failedWindow = loadPlayback();
|
||||
failedWindow.slopsmith.playback.registerTransportAdapter(makeAdapter({ seekError: 'seek failed' }));
|
||||
failedWindow.feedBack.playback.registerTransportAdapter(makeAdapter({ seekError: 'seek failed' }));
|
||||
await dispatch(failedWindow, 'start', { authorization: 'user-action', requesterId: 'core.player.controls', target: makeTarget() });
|
||||
const failed = await dispatch(failedWindow, 'seek', { requesterId: 'core.player.controls', time: 3 });
|
||||
assert.equal(failed.status, 'failed');
|
||||
@@ -178,26 +178,26 @@ test('ended transport events and seek failure/rollback outcomes are distinguisha
|
||||
assert.equal(diagnosticsSnapshot(failedWindow).state.state, 'paused');
|
||||
|
||||
const unsupportedWindow = loadPlayback();
|
||||
unsupportedWindow.slopsmith.playback.registerTransportAdapter({ inspect: () => ({ currentTime: 0, duration: 120, isPlaying: true }), start: () => ({ currentTime: 0, duration: 120, isPlaying: true }) });
|
||||
unsupportedWindow.feedBack.playback.registerTransportAdapter({ inspect: () => ({ currentTime: 0, duration: 120, isPlaying: true }), start: () => ({ currentTime: 0, duration: 120, isPlaying: true }) });
|
||||
await dispatch(unsupportedWindow, 'start', { authorization: 'user-action', requesterId: 'core.player.controls', target: makeTarget() });
|
||||
const unsupported = await dispatch(unsupportedWindow, 'seek', { requesterId: 'core.player.controls', time: 3 });
|
||||
assert.equal(unsupported.status, 'unsupported-command');
|
||||
assert.equal(diagnosticsSnapshot(unsupportedWindow).state.state, 'playing');
|
||||
|
||||
const malformedWindow = loadPlayback();
|
||||
malformedWindow.slopsmith.playback.registerTransportAdapter(makeAdapter({ seekResult: { completed: true, from: 2, to: NaN } }));
|
||||
malformedWindow.feedBack.playback.registerTransportAdapter(makeAdapter({ seekResult: { completed: true, from: 2, to: NaN } }));
|
||||
await dispatch(malformedWindow, 'start', { authorization: 'user-action', requesterId: 'core.player.controls', target: makeTarget() });
|
||||
const malformed = await dispatch(malformedWindow, 'seek', { requesterId: 'core.player.controls', time: 6 });
|
||||
assert.equal(malformed.status, 'failed');
|
||||
assert.match(malformed.reason, /malformed seek result/i);
|
||||
|
||||
window.slopsmith.playback.transportEvent('ended', { requesterId: 'core.player.controls', currentTime: 120 });
|
||||
window.feedBack.playback.transportEvent('ended', { requesterId: 'core.player.controls', currentTime: 120 });
|
||||
assert.equal(diagnosticsSnapshot(window).state.state, 'ended');
|
||||
});
|
||||
|
||||
test('invalid loop boundaries do not mutate an active loop', async () => {
|
||||
const window = loadPlayback();
|
||||
window.slopsmith.playback.registerTransportAdapter(makeAdapter());
|
||||
window.feedBack.playback.registerTransportAdapter(makeAdapter());
|
||||
await dispatch(window, 'start', { authorization: 'user-action', requesterId: 'core.player.controls', target: makeTarget() });
|
||||
await dispatch(window, 'set-loop', { requesterId: 'core.player.controls', startTime: 2, endTime: 4 });
|
||||
|
||||
@@ -212,7 +212,7 @@ test('invalid loop boundaries do not mutate an active loop', async () => {
|
||||
|
||||
test('normal resume is denied after a user-priority pause until a user action resumes', async () => {
|
||||
const window = loadPlayback();
|
||||
window.slopsmith.playback.registerTransportAdapter(makeAdapter());
|
||||
window.feedBack.playback.registerTransportAdapter(makeAdapter());
|
||||
await dispatch(window, 'start', { authorization: 'user-action', requesterId: 'core.player.controls', target: makeTarget() });
|
||||
await dispatch(window, 'pause', { requesterId: 'core.player.controls', priority: 'user' });
|
||||
|
||||
@@ -225,7 +225,7 @@ test('normal resume is denied after a user-priority pause until a user action re
|
||||
|
||||
test('stale and cancelled operations are reported distinctly', async () => {
|
||||
const window = loadPlayback();
|
||||
window.slopsmith.playback.registerTransportAdapter(makeAdapter({ seekResult: { completed: false, from: 2, to: NaN } }));
|
||||
window.feedBack.playback.registerTransportAdapter(makeAdapter({ seekResult: { completed: false, from: 2, to: NaN } }));
|
||||
await dispatch(window, 'start', { authorization: 'user-action', requesterId: 'core.player.controls', target: makeTarget() });
|
||||
const sessionId = diagnosticsSnapshot(window).state.sessionId;
|
||||
|
||||
|
||||
@@ -27,16 +27,16 @@ function loadInspector(window) {
|
||||
|
||||
function captureEvents(window, eventName) {
|
||||
const events = [];
|
||||
window.slopsmith.on(eventName, event => events.push(event.detail));
|
||||
window.feedBack.on(eventName, event => events.push(event.detail));
|
||||
return events;
|
||||
}
|
||||
|
||||
function diagnosticsSnapshot(window, options = {}) {
|
||||
return window.slopsmith.playback.snapshot(options);
|
||||
return window.feedBack.playback.snapshot(options);
|
||||
}
|
||||
|
||||
function dispatch(window, command, payload = {}, requester = 'test') {
|
||||
return window.slopsmith.capabilities.dispatch({ capability: 'playback', command, args: payload, requester });
|
||||
return window.feedBack.capabilities.dispatch({ capability: 'playback', command, args: payload, requester });
|
||||
}
|
||||
|
||||
function makeTarget(overrides = {}) {
|
||||
|
||||
@@ -21,7 +21,7 @@ const SRC = path.join(__dirname, '..', '..', 'static', 'v3', 'plugins-page.js');
|
||||
function loadPage(opts) {
|
||||
opts = opts || {};
|
||||
const store = opts.store || {};
|
||||
const win = { slopsmith: null };
|
||||
const win = { feedBack: null };
|
||||
win.localStorage = {
|
||||
getItem: (k) => (k in store ? store[k] : null),
|
||||
setItem: (k, v) => { if (opts.throwOnSet) throw new Error('quota'); store[k] = String(v); },
|
||||
|
||||
@@ -129,7 +129,7 @@ function createProgressWindow(progressionState) {
|
||||
refresh() { return Promise.resolve(progressionState); },
|
||||
},
|
||||
playSong() {},
|
||||
slopsmith: {
|
||||
feedBack: {
|
||||
on(type, handler) {
|
||||
const list = listeners.get(type) || [];
|
||||
list.push(handler);
|
||||
@@ -163,14 +163,14 @@ test('progression:calibration-attempt with 0.92 shows retry overlay and So close
|
||||
mastery_rank: 0,
|
||||
onboarding: {
|
||||
calibration_status: 'pending',
|
||||
diagnostic_filename: 'diagnostics-builtin/slopsmith-diagnostic-basic-guitar.sloppak',
|
||||
diagnostic_filename: 'diagnostics-builtin/feedBack-diagnostic-basic-guitar.sloppak',
|
||||
},
|
||||
paths: [],
|
||||
wallet: { balance: 0, lifetime_db: 0 },
|
||||
quests: {},
|
||||
});
|
||||
|
||||
win.slopsmith.emit('progression:calibration-attempt', { accuracy: 0.92 });
|
||||
win.feedBack.emit('progression:calibration-attempt', { accuracy: 0.92 });
|
||||
|
||||
const overlay = win.document.getElementById('v3-calibration-retry');
|
||||
assert.ok(overlay, 'retry overlay should exist');
|
||||
@@ -184,14 +184,14 @@ test('progression:calibration-completed shows success overlay and Setup verified
|
||||
mastery_rank: 0,
|
||||
onboarding: {
|
||||
calibration_status: 'pending',
|
||||
diagnostic_filename: 'diagnostics-builtin/slopsmith-diagnostic-basic-guitar.sloppak',
|
||||
diagnostic_filename: 'diagnostics-builtin/feedBack-diagnostic-basic-guitar.sloppak',
|
||||
},
|
||||
paths: [],
|
||||
wallet: { balance: 0, lifetime_db: 0 },
|
||||
quests: {},
|
||||
});
|
||||
|
||||
win.slopsmith.emit('progression:calibration-completed', {});
|
||||
win.feedBack.emit('progression:calibration-completed', {});
|
||||
|
||||
const overlay = win.document.getElementById('v3-calibration-success');
|
||||
assert.ok(overlay, 'success overlay should exist');
|
||||
@@ -205,14 +205,14 @@ test('success overlay for skipped state does not claim rank-up', () => {
|
||||
mastery_rank: 1,
|
||||
onboarding: {
|
||||
calibration_status: 'skipped',
|
||||
diagnostic_filename: 'diagnostics-builtin/slopsmith-diagnostic-basic-guitar.sloppak',
|
||||
diagnostic_filename: 'diagnostics-builtin/feedBack-diagnostic-basic-guitar.sloppak',
|
||||
},
|
||||
paths: [],
|
||||
wallet: { balance: 0, lifetime_db: 0 },
|
||||
quests: {},
|
||||
});
|
||||
|
||||
win.slopsmith.emit('progression:calibration-completed', {});
|
||||
win.feedBack.emit('progression:calibration-completed', {});
|
||||
|
||||
const overlay = win.document.getElementById('v3-calibration-success');
|
||||
assert.ok(overlay);
|
||||
@@ -226,15 +226,15 @@ test('calibration-completed does not stack duplicate success overlays', () => {
|
||||
mastery_rank: 1,
|
||||
onboarding: {
|
||||
calibration_status: 'skipped',
|
||||
diagnostic_filename: 'diagnostics-builtin/slopsmith-diagnostic-basic-guitar.sloppak',
|
||||
diagnostic_filename: 'diagnostics-builtin/feedBack-diagnostic-basic-guitar.sloppak',
|
||||
},
|
||||
paths: [],
|
||||
wallet: { balance: 0, lifetime_db: 0 },
|
||||
quests: {},
|
||||
});
|
||||
|
||||
win.slopsmith.emit('progression:calibration-completed', {});
|
||||
win.slopsmith.emit('progression:calibration-completed', {});
|
||||
win.feedBack.emit('progression:calibration-completed', {});
|
||||
win.feedBack.emit('progression:calibration-completed', {});
|
||||
|
||||
const overlays = win.__bodyChildren.filter((el) => el.id === 'v3-calibration-success');
|
||||
assert.equal(overlays.length, 1);
|
||||
|
||||
@@ -54,7 +54,7 @@ function load(progressionState) {
|
||||
// Deliver a CustomEvent-like wrapper ({detail}), exactly as the real bus
|
||||
// does (capabilities.js: bus.on → addEventListener, fn gets a CustomEvent).
|
||||
// Test call sites pass the raw payload; the handler must unwrap e.detail.
|
||||
sandbox.window.slopsmith = { on: (name, fn) => { handlers[name] = (payload) => fn({ detail: payload }); } };
|
||||
sandbox.window.feedBack = { on: (name, fn) => { handlers[name] = (payload) => fn({ detail: payload }); } };
|
||||
sandbox.window.v3Progression = { get: () => progressionState };
|
||||
vm.createContext(sandbox);
|
||||
vm.runInContext(SRC, sandbox);
|
||||
|
||||
@@ -23,7 +23,7 @@ function load(states) {
|
||||
fetch: async () => ({ ok: true, json: async () => states[Math.min(i++, states.length - 1)] }),
|
||||
};
|
||||
sandbox.window = sandbox;
|
||||
sandbox.window.slopsmith = { emit: (name, detail) => events.push({ name, detail }) };
|
||||
sandbox.window.feedBack = { emit: (name, detail) => events.push({ name, detail }) };
|
||||
sandbox.document = { readyState: 'complete', addEventListener: () => {} };
|
||||
vm.createContext(sandbox);
|
||||
vm.runInContext(SRC, sandbox);
|
||||
|
||||
@@ -62,10 +62,10 @@ function loadClose(sandbox, src) {
|
||||
vm.runInContext(code, sandbox);
|
||||
}
|
||||
|
||||
test('closeCurrentSong is exported on window and window.slopsmith', () => {
|
||||
test('closeCurrentSong is exported on window and window.feedBack', () => {
|
||||
const src = fs.readFileSync(APP_JS, 'utf8');
|
||||
assert.match(src, /window\.closeCurrentSong\s*=\s*closeCurrentSong/);
|
||||
assert.match(src, /window\.slopsmith\.closeCurrentSong\s*=\s*closeCurrentSong/);
|
||||
assert.match(src, /window\.feedBack\.closeCurrentSong\s*=\s*closeCurrentSong/);
|
||||
});
|
||||
|
||||
test('closeCurrentSong uses _playerOriginScreen when set', async () => {
|
||||
|
||||
@@ -112,8 +112,8 @@ test('every song:play/pause/ended emit uses _songEventPayload', () => {
|
||||
const src = fs.readFileSync(APP_JS, 'utf8');
|
||||
const lines = src.split('\n');
|
||||
// Accept aliased calls like `sm.emit(...)` (the JUCE shim caches
|
||||
// window.slopsmith in `sm`) — not just literal `window.slopsmith.emit`.
|
||||
const emitRe = /(?:window\.slopsmith|\w+)\.emit\(\s*['"]song:(play|pause|ended)['"]/;
|
||||
// window.feedBack in `sm`) — not just literal `window.feedBack.emit`.
|
||||
const emitRe = /(?:window\.feedBack|\w+)\.emit\(\s*['"]song:(play|pause|ended)['"]/;
|
||||
const okRe = /_songEventPayload\(\)|,\s*payload\s*\)/;
|
||||
const offending = [];
|
||||
for (const line of lines) {
|
||||
@@ -134,7 +134,7 @@ test('there are at least 8 song:* emit sites threaded through the helper', () =>
|
||||
// count drops, someone removed an emit (regression) or refactored an
|
||||
// event away (intentional — this test then needs updating).
|
||||
const src = fs.readFileSync(APP_JS, 'utf8');
|
||||
const matches = src.match(/(?:window\.slopsmith|\w+)\.emit\(\s*['"]song:(play|pause|ended)['"][^)]*\)/g) || [];
|
||||
const matches = src.match(/(?:window\.feedBack|\w+)\.emit\(\s*['"]song:(play|pause|ended)['"][^)]*\)/g) || [];
|
||||
assert.ok(
|
||||
matches.length >= 8,
|
||||
`expected ≥8 song:* emits, found ${matches.length}`,
|
||||
|
||||
@@ -27,7 +27,7 @@ function buildSandbox({ loopA = null, loopB = null, isPlaying = false } = {}) {
|
||||
__togglePlayCalls: 0,
|
||||
__clearLoopCalls: 0,
|
||||
window: {
|
||||
slopsmith: {
|
||||
feedBack: {
|
||||
getLoop() {
|
||||
return { loopA: sandbox.loopA, loopB: sandbox.loopB };
|
||||
},
|
||||
@@ -68,10 +68,10 @@ function loadRestart(sandbox, src, { audioSeekImpl } = {}) {
|
||||
vm.runInContext(code, sandbox);
|
||||
}
|
||||
|
||||
test('restartCurrentSong is exported on window and window.slopsmith', () => {
|
||||
test('restartCurrentSong is exported on window and window.feedBack', () => {
|
||||
const src = fs.readFileSync(APP_JS, 'utf8');
|
||||
assert.match(src, /window\.restartCurrentSong\s*=\s*restartCurrentSong/);
|
||||
assert.match(src, /window\.slopsmith\.restartCurrentSong\s*=\s*restartCurrentSong/);
|
||||
assert.match(src, /window\.feedBack\.restartCurrentSong\s*=\s*restartCurrentSong/);
|
||||
});
|
||||
|
||||
test('no loop: seeks to 0 with song-restart and starts playback when stopped', async () => {
|
||||
|
||||
@@ -55,7 +55,7 @@ function buildSandbox({ juceMode = false, currentTime = 10, duration = Infinity
|
||||
jucePlayer,
|
||||
window: {
|
||||
_juceMode: juceMode,
|
||||
slopsmith: {
|
||||
feedBack: {
|
||||
emit(event, detail) { emitCalls.push({ event, detail }); },
|
||||
},
|
||||
},
|
||||
|
||||
@@ -74,11 +74,11 @@ function buildSandbox({ juceMode = false } = {}) {
|
||||
_juceAudioUrl: juceMode ? '/audio/old-song.ogg' : null,
|
||||
_currentSongAudio: { url: '/audio/old-song.ogg' },
|
||||
_clearJuceRerouteMemo() {},
|
||||
slopsmith: {
|
||||
feedBack: {
|
||||
isPlaying: true,
|
||||
emit() {},
|
||||
},
|
||||
slopsmithDesktop: {
|
||||
feedBackDesktop: {
|
||||
audio: {
|
||||
setBackingSpeed(rate) {
|
||||
backingCalls.push(['setBackingSpeed', rate]);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Source-level guards for the consolidated tour menu (slopsmith#272).
|
||||
// The engine lives in a DOMContentLoaded handler that wires window.slopsmith,
|
||||
// Source-level guards for the consolidated tour menu (feedBack#272).
|
||||
// The engine lives in a DOMContentLoaded handler that wires window.feedBack,
|
||||
// localStorage, and Shepherd — too much browser surface to reproduce cleanly
|
||||
// in a vm sandbox. These checks lock in the contract (viz relevance gating,
|
||||
// complete-vs-cancel semantics, waitFor validation, focus management,
|
||||
@@ -115,9 +115,9 @@ test('_updateMenuVisibility dismisses orphan toast when relevance drops to zero'
|
||||
|
||||
test('popover has role=dialog with aria-controls wired from the trigger', () => {
|
||||
const fn = extractBlock(SRC, 'function _ensureMenu()');
|
||||
assert.match(fn, /setAttribute\(\s*['"]aria-controls['"]\s*,\s*['"]slopsmith-tour-menu-popover['"]/,
|
||||
assert.match(fn, /setAttribute\(\s*['"]aria-controls['"]\s*,\s*['"]feedBack-tour-menu-popover['"]/,
|
||||
'trigger must wire aria-controls to the popover id');
|
||||
assert.match(fn, /_menuPopover\.id\s*=\s*['"]slopsmith-tour-menu-popover['"]/,
|
||||
assert.match(fn, /_menuPopover\.id\s*=\s*['"]feedBack-tour-menu-popover['"]/,
|
||||
'popover must carry the matching id');
|
||||
assert.match(fn, /setAttribute\(\s*['"]role['"]\s*,\s*['"]dialog['"]/,
|
||||
'popover must use role=dialog (not the menu role we don\'t implement)');
|
||||
|
||||
@@ -12,19 +12,19 @@ const TUNER_SCREEN_JS = path.join(__dirname, '..', '..', 'plugins', 'tuner', 'sc
|
||||
function loadTuningHelpers() {
|
||||
const src = fs.readFileSync(APP_JS, 'utf8');
|
||||
const start = src.indexOf('function isBassArrangement(');
|
||||
const endMarker = 'window.slopsmith.parseRawTuningOffsets = parseRawTuningOffsets;';
|
||||
const endMarker = 'window.feedBack.parseRawTuningOffsets = parseRawTuningOffsets;';
|
||||
const end = src.indexOf(endMarker);
|
||||
if (start === -1 || end === -1) throw new Error('tuning helper block not found in app.js');
|
||||
const sandbox = { window: { slopsmith: {} }, exports: {} };
|
||||
const sandbox = { window: { feedBack: {} }, exports: {} };
|
||||
vm.createContext(sandbox);
|
||||
vm.runInContext(
|
||||
src.slice(start, end + endMarker.length),
|
||||
sandbox
|
||||
);
|
||||
return sandbox.window.slopsmith;
|
||||
return sandbox.window.feedBack;
|
||||
}
|
||||
|
||||
const slopsmithHelpers = loadTuningHelpers();
|
||||
const feedBackHelpers = loadTuningHelpers();
|
||||
|
||||
function createTunerSandbox() {
|
||||
const enableCalls = [];
|
||||
@@ -101,7 +101,7 @@ function createTunerSandbox() {
|
||||
__setPlayerActive(v) { playerActive = v; },
|
||||
__setSongInfo(info) {
|
||||
songInfo = info;
|
||||
sandbox.window.slopsmith.currentSong = info ? {
|
||||
sandbox.window.feedBack.currentSong = info ? {
|
||||
filename: info.filename || 'song.sloppak',
|
||||
arrangementIndex: info.arrangement_index,
|
||||
tuning: info.tuning,
|
||||
@@ -111,8 +111,8 @@ function createTunerSandbox() {
|
||||
};
|
||||
|
||||
sandbox.window = sandbox;
|
||||
sandbox.window.slopsmith = {
|
||||
...slopsmithHelpers,
|
||||
sandbox.window.feedBack = {
|
||||
...feedBackHelpers,
|
||||
on() {},
|
||||
off() {},
|
||||
currentSong: null,
|
||||
@@ -318,8 +318,8 @@ test('song:loading clears dismiss state for next load', async () => {
|
||||
test('screen.js registers song:loading and song:ready auto-open listeners at boot', () => {
|
||||
const src = fs.readFileSync(TUNER_SCREEN_JS, 'utf8');
|
||||
assert.match(src, /function _installAutoOpenListeners/);
|
||||
assert.match(src, /window\.slopsmith\.on\('song:loading', _onAutoOpenSongLoading\)/);
|
||||
assert.match(src, /window\.slopsmith\.on\('song:ready', _onAutoOpenSongReady\)/);
|
||||
assert.match(src, /window\.feedBack\.on\('song:loading', _onAutoOpenSongLoading\)/);
|
||||
assert.match(src, /window\.feedBack\.on\('song:ready', _onAutoOpenSongReady\)/);
|
||||
assert.match(src, /function _tuningIdentityKey/);
|
||||
assert.doesNotMatch(src, /restartCurrentSong/);
|
||||
});
|
||||
|
||||
@@ -35,7 +35,7 @@ function loadTuningDisplayHelpers() {
|
||||
extractBlock(src, 'function parseRawTuningOffsets('),
|
||||
extractBlock(src, 'function displayTuningName('),
|
||||
].join('\n');
|
||||
const sandbox = { window: { slopsmith: {} }, exports: {} };
|
||||
const sandbox = { window: { feedBack: {} }, exports: {} };
|
||||
vm.createContext(sandbox);
|
||||
vm.runInContext(block + '\nexports.displayTuningName = displayTuningName;', sandbox);
|
||||
return sandbox.exports;
|
||||
@@ -57,7 +57,7 @@ test('displayTuningName sanitizes raw offset strings to Custom Tuning', () => {
|
||||
assert.equal(displayTuningName('-3,-1,0,1,2,3'), 'Custom Tuning');
|
||||
});
|
||||
|
||||
test('displayTuningName names a known raw offset string (slopsmith#867)', () => {
|
||||
test('displayTuningName names a known raw offset string (feedBack#867)', () => {
|
||||
// Now that the API serves raw offsets, a known tuning passed as a raw
|
||||
// string must resolve to its real name, not collapse to Custom Tuning.
|
||||
assert.equal(displayTuningName('-1 -1 -1 -1 -1 -1'), 'Eb Standard');
|
||||
@@ -67,7 +67,7 @@ test('displayTuningName names a known raw offset string (slopsmith#867)', () =>
|
||||
assert.equal(displayTuningName('-2 0 0 0 -2 1'), 'Custom Tuning');
|
||||
});
|
||||
|
||||
test('displayTuningName recognizes 4/5-string uniform standard (slopsmith#867)', () => {
|
||||
test('displayTuningName recognizes 4/5-string uniform standard (feedBack#867)', () => {
|
||||
// A normal 4-string bass [0,0,0,0] must not fall through to Custom Tuning.
|
||||
assert.equal(displayTuningName(null, [0, 0, 0, 0]), 'E Standard');
|
||||
assert.equal(displayTuningName(null, [-2, -2, -2, -2]), 'D Standard');
|
||||
|
||||
@@ -16,10 +16,10 @@ const V2_HTML = path.join(__dirname, '..', '..', 'static', 'index.html');
|
||||
function loadTuningHelpers() {
|
||||
const src = fs.readFileSync(APP_JS, 'utf8');
|
||||
const start = src.indexOf('function isBassArrangement(');
|
||||
const endMarker = 'window.slopsmith.parseRawTuningOffsets = parseRawTuningOffsets;';
|
||||
const endMarker = 'window.feedBack.parseRawTuningOffsets = parseRawTuningOffsets;';
|
||||
const end = src.indexOf(endMarker);
|
||||
if (start === -1 || end === -1) throw new Error('tuning helper block not found in app.js');
|
||||
const sandbox = { window: { slopsmith: {} }, exports: {} };
|
||||
const sandbox = { window: { feedBack: {} }, exports: {} };
|
||||
vm.createContext(sandbox);
|
||||
vm.runInContext(
|
||||
src.slice(start, end + endMarker.length) + '\n'
|
||||
|
||||
@@ -28,10 +28,10 @@ function extractBlock(src, startMarker) {
|
||||
function loadTuningHelpers() {
|
||||
const src = fs.readFileSync(APP_JS, 'utf8');
|
||||
const start = src.indexOf('function _looksLikeRawTuningOffsets(');
|
||||
const endMarker = 'window.slopsmith.parseRawTuningOffsets = parseRawTuningOffsets;';
|
||||
const endMarker = 'window.feedBack.parseRawTuningOffsets = parseRawTuningOffsets;';
|
||||
const end = src.indexOf(endMarker);
|
||||
if (start === -1 || end === -1) throw new Error('tuning helpers not found');
|
||||
const sandbox = { window: { slopsmith: {} }, exports: {} };
|
||||
const sandbox = { window: { feedBack: {} }, exports: {} };
|
||||
vm.createContext(sandbox);
|
||||
vm.runInContext(
|
||||
src.slice(start, end + endMarker.length) + '\n'
|
||||
|
||||
@@ -23,12 +23,12 @@ test('venueMoodClassForState maps performance states', () => {
|
||||
assert.equal(venue.venueMoodClassForState('unknown'), 'venue-mood-state-idle');
|
||||
});
|
||||
|
||||
test('setting persistence key is slopsmith-venue-mood-fx', () => {
|
||||
assert.equal(venue.KEY, 'slopsmith-venue-mood-fx');
|
||||
test('setting persistence key is feedBack-venue-mood-fx', () => {
|
||||
assert.equal(venue.KEY, 'feedBack-venue-mood-fx');
|
||||
});
|
||||
|
||||
test('venue motion setting key is slopsmith-venue-motion', () => {
|
||||
assert.equal(venue.MOTION_KEY, 'slopsmith-venue-motion');
|
||||
test('venue motion setting key is feedBack-venue-motion', () => {
|
||||
assert.equal(venue.MOTION_KEY, 'feedBack-venue-motion');
|
||||
assert.equal(venue.MOTION_DEFAULT, 'subtle');
|
||||
});
|
||||
|
||||
@@ -296,7 +296,7 @@ test('venue visualization session hides strip and marks scene pending', () => {
|
||||
sceneWash.classList.add('hidden');
|
||||
const vizPicker = new El();
|
||||
vizPicker.value = 'venue';
|
||||
const storage = new Map([['slopsmith-venue-mood-fx', 'full']]);
|
||||
const storage = new Map([['feedBack-venue-mood-fx', 'full']]);
|
||||
const origDocument = global.document;
|
||||
global.document = {
|
||||
getElementById(id) {
|
||||
@@ -404,7 +404,7 @@ test('window.v3VenueMoodFx exposes getState with venue flag', () => {
|
||||
assert.equal(typeof st.isVenueVisualization, 'boolean');
|
||||
});
|
||||
|
||||
test('setMotion persists slopsmith-venue-motion and syncs renderer', () => {
|
||||
test('setMotion persists feedBack-venue-motion and syncs renderer', () => {
|
||||
const storage = new Map();
|
||||
let synced = null;
|
||||
global.localStorage = {
|
||||
@@ -416,7 +416,7 @@ test('setMotion persists slopsmith-venue-motion and syncs renderer', () => {
|
||||
global.document = { getElementById: () => null };
|
||||
try {
|
||||
assert.equal(venue.setMotion('full'), 'full');
|
||||
assert.equal(storage.get('slopsmith-venue-motion'), 'full');
|
||||
assert.equal(storage.get('feedBack-venue-motion'), 'full');
|
||||
assert.equal(synced, 'full');
|
||||
assert.equal(venue.setMotion('nope'), 'subtle');
|
||||
assert.equal(synced, 'subtle');
|
||||
@@ -448,7 +448,7 @@ test('bindRuntime wires venue motion select', () => {
|
||||
let synced = null;
|
||||
globalThis.h3dVenueSceneSetMotionMode = (mode) => { synced = mode; };
|
||||
global.localStorage = {
|
||||
getItem(k) { return k === 'slopsmith-venue-motion' ? 'subtle' : null; },
|
||||
getItem(k) { return k === 'feedBack-venue-motion' ? 'subtle' : null; },
|
||||
setItem() {},
|
||||
};
|
||||
global.document = {
|
||||
|
||||
@@ -119,7 +119,7 @@ test('venue-scene-3d syncs instrument POV from arrangement signal', () => {
|
||||
global.highway = { getSongInfo: () => ({ arrangement: 'Bass' }) };
|
||||
global.v3VenueViz = venueViz;
|
||||
global.v3VenueInstrumentPov = pov;
|
||||
global.slopsmith = { on() {} };
|
||||
global.feedBack = { on() {} };
|
||||
try {
|
||||
venueScene.activate();
|
||||
assert.equal(global._venuePovInput, 'Bass');
|
||||
@@ -140,7 +140,7 @@ test('venue-scene-3d syncs instrument POV from arrangement signal', () => {
|
||||
delete global.highway;
|
||||
delete global.v3VenueViz;
|
||||
delete global.v3VenueInstrumentPov;
|
||||
delete global.slopsmith;
|
||||
delete global.feedBack;
|
||||
delete global._venuePovInput;
|
||||
}
|
||||
});
|
||||
@@ -156,7 +156,7 @@ test('lyrics visibility during guitar practice does not force vocals POV', () =>
|
||||
};
|
||||
global.v3VenueViz = venueViz;
|
||||
global.v3VenueInstrumentPov = pov;
|
||||
global.slopsmith = { on() {} };
|
||||
global.feedBack = { on() {} };
|
||||
try {
|
||||
venueScene.activate();
|
||||
assert.equal(global._venuePovInput, 'Lead');
|
||||
@@ -174,7 +174,7 @@ test('lyrics visibility during guitar practice does not force vocals POV', () =>
|
||||
delete global.highway;
|
||||
delete global.v3VenueViz;
|
||||
delete global.v3VenueInstrumentPov;
|
||||
delete global.slopsmith;
|
||||
delete global.feedBack;
|
||||
delete global._venuePovInput;
|
||||
}
|
||||
});
|
||||
@@ -212,7 +212,7 @@ test('syncViz activates only for venue visualization id', () => {
|
||||
global.h3dVenueSceneGetState = () => ({ active: !!global._h3dActive, assetsLoaded: false, loadFailed: false });
|
||||
global.v3VenueViz = venueViz;
|
||||
global.v3VenueInstrumentPov = pov;
|
||||
global.slopsmith = { on() {} };
|
||||
global.feedBack = { on() {} };
|
||||
try {
|
||||
venueScene.deactivate();
|
||||
venueScene.syncViz('highway_3d');
|
||||
@@ -229,7 +229,7 @@ test('syncViz activates only for venue visualization id', () => {
|
||||
delete global.h3dVenueSceneGetState;
|
||||
delete global.v3VenueViz;
|
||||
delete global.v3VenueInstrumentPov;
|
||||
delete global.slopsmith;
|
||||
delete global.feedBack;
|
||||
delete global._h3dActive;
|
||||
delete global._h3dMood;
|
||||
}
|
||||
@@ -262,7 +262,7 @@ test('live performance state forwards mood to highway venue scene API', () => {
|
||||
isVenueVisualization: () => true,
|
||||
readVizSelection: () => 'venue',
|
||||
};
|
||||
global.slopsmith = { on() {} };
|
||||
global.feedBack = { on() {} };
|
||||
try {
|
||||
venueScene.activate();
|
||||
venueScene.onPerformanceState({ detail: { state: 'fire' } });
|
||||
@@ -276,7 +276,7 @@ test('live performance state forwards mood to highway venue scene API', () => {
|
||||
delete global.h3dVenueSceneSetInstrumentPov;
|
||||
delete global.h3dVenueSceneGetState;
|
||||
delete global.v3VenueViz;
|
||||
delete global.slopsmith;
|
||||
delete global.feedBack;
|
||||
delete global._mood;
|
||||
}
|
||||
});
|
||||
@@ -325,7 +325,7 @@ test('venue-scene-3d syncs motion on activate', () => {
|
||||
global.v3VenueMoodFx = { getMotion: () => 'full' };
|
||||
global.v3VenueViz = venueViz;
|
||||
global.v3VenueInstrumentPov = pov;
|
||||
global.slopsmith = { on() {} };
|
||||
global.feedBack = { on() {} };
|
||||
try {
|
||||
venueScene.activate();
|
||||
assert.equal(global._venueMotion, 'full');
|
||||
@@ -342,7 +342,7 @@ test('venue-scene-3d syncs motion on activate', () => {
|
||||
delete global.v3VenueMoodFx;
|
||||
delete global.v3VenueViz;
|
||||
delete global.v3VenueInstrumentPov;
|
||||
delete global.slopsmith;
|
||||
delete global.feedBack;
|
||||
delete global._venueMotion;
|
||||
}
|
||||
});
|
||||
@@ -360,7 +360,7 @@ test('runtime safety: motion pass does not touch scoring detection timing or aud
|
||||
'plugins/scoring',
|
||||
'static/audio',
|
||||
'AudioEngine',
|
||||
'slopsmith-desktop',
|
||||
'feedBack-desktop',
|
||||
];
|
||||
const allowedTouched = [
|
||||
'plugins/highway_3d/screen.js',
|
||||
|
||||
@@ -145,7 +145,7 @@ test('app.js adds Venue visualization option and adapter', () => {
|
||||
assert.match(src, /opt\.value = 'venue'/);
|
||||
assert.match(src, /opt\.textContent = 'Venue'/);
|
||||
assert.match(src, /if \(id === 'venue'\)/);
|
||||
assert.match(src, /slopsmithViz_highway_3d/);
|
||||
assert.match(src, /feedBackViz_highway_3d/);
|
||||
assert.match(src, /localStorage\.setItem\('vizSelection', 'venue'\)/);
|
||||
assert.match(src, /_installVizRenderer\(venueRenderer, 'highway_3d'\)/);
|
||||
assert.match(src, /onVenueVisualizationSelected/);
|
||||
@@ -170,15 +170,15 @@ test('onVenueVisualizationSelected defaults mood to full only on first selection
|
||||
};
|
||||
try {
|
||||
// No stored preference yet → default the mood to FULL.
|
||||
storage.delete('slopsmith-venue-mood-fx');
|
||||
storage.delete('feedBack-venue-mood-fx');
|
||||
venue.onVenueVisualizationSelected();
|
||||
assert.equal(venue.get(), 'full');
|
||||
// An explicit 'subtle' choice must be preserved, not clobbered to full.
|
||||
storage.set('slopsmith-venue-mood-fx', 'subtle');
|
||||
storage.set('feedBack-venue-mood-fx', 'subtle');
|
||||
venue.onVenueVisualizationSelected();
|
||||
assert.equal(venue.get(), 'subtle');
|
||||
// 'off' likewise preserved.
|
||||
storage.set('slopsmith-venue-mood-fx', 'off');
|
||||
storage.set('feedBack-venue-mood-fx', 'off');
|
||||
venue.onVenueVisualizationSelected();
|
||||
assert.equal(venue.get(), 'off');
|
||||
} finally {
|
||||
@@ -213,7 +213,7 @@ test('venue mood source documents strip overlay disabled', () => {
|
||||
test('app.js preserves plugin viz population for drum/tab/piano highways', () => {
|
||||
const src = fs.readFileSync(APP_JS, 'utf8');
|
||||
assert.match(src, /p\.type === 'visualization'/);
|
||||
assert.match(src, /slopsmithViz_/);
|
||||
assert.match(src, /feedBackViz_/);
|
||||
assert.match(src, /BUILTIN_OPT_VALUES/);
|
||||
});
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ function captureEvents(api, eventNames) {
|
||||
|
||||
test('visualization domain registers an active provider-coordinator owner', () => {
|
||||
const window = loadVisualization();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
const pipeline = api.inspect('visualization');
|
||||
assert.ok(pipeline, 'visualization pipeline exists');
|
||||
const owner = (pipeline.participants || []).find(p => p.pluginId === 'core.visualization');
|
||||
@@ -36,12 +36,12 @@ test('visualization domain registers an active provider-coordinator owner', () =
|
||||
[...owner.commands].sort(),
|
||||
['clear-renderer', 'inspect', 'list-providers', 'select-renderer'],
|
||||
);
|
||||
assert.equal(window.slopsmith.vizDomain.version, 1);
|
||||
assert.equal(window.feedBack.vizDomain.version, 1);
|
||||
});
|
||||
|
||||
test('visualization is no longer a reserved future domain', async () => {
|
||||
const window = loadVisualization();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
const result = await api.dispatch({ capability: 'visualization', command: 'inspect', source: 'test' });
|
||||
assert.equal(result.outcome, 'handled');
|
||||
assert.equal(result.payload.current, 'default');
|
||||
@@ -49,14 +49,14 @@ test('visualization is no longer a reserved future domain', async () => {
|
||||
|
||||
test('refreshProviders registers participants with factory metadata', () => {
|
||||
const window = loadVisualization();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
const highway3d = () => ({});
|
||||
highway3d.contextType = 'webgl2';
|
||||
highway3d.matchesArrangement = () => true;
|
||||
window.slopsmithViz_highway_3d = highway3d;
|
||||
window.slopsmithViz_piano = () => ({});
|
||||
window.feedBackViz_highway_3d = highway3d;
|
||||
window.feedBackViz_piano = () => ({});
|
||||
|
||||
window.slopsmith.vizDomain.refreshProviders([
|
||||
window.feedBack.vizDomain.refreshProviders([
|
||||
{ id: 'highway_3d', label: '3D Highway' },
|
||||
{ id: 'piano', label: 'Piano' },
|
||||
{ id: 'auto', label: 'Auto' }, // built-ins are skipped
|
||||
@@ -68,7 +68,7 @@ test('refreshProviders registers participants with factory metadata', () => {
|
||||
assert.ok(ids.includes('highway_3d') && ids.includes('piano'));
|
||||
assert.ok(!ids.includes('auto') && !ids.includes('default'));
|
||||
|
||||
const snapshot = window.slopsmith.vizDomain.snapshot();
|
||||
const snapshot = window.feedBack.vizDomain.snapshot();
|
||||
const h3d = snapshot.providers.find(p => p.id === 'highway_3d');
|
||||
assert.equal(h3d.contextType, 'webgl2');
|
||||
assert.equal(h3d.claims, true);
|
||||
@@ -79,9 +79,9 @@ test('refreshProviders registers participants with factory metadata', () => {
|
||||
|
||||
test('refreshProviders surfaces declared per-instance settings in the snapshot', () => {
|
||||
const window = loadVisualization();
|
||||
const caps = window.slopsmith.capabilities;
|
||||
window.slopsmithViz_highway_3d = () => ({});
|
||||
window.slopsmithViz_piano = () => ({});
|
||||
const caps = window.feedBack.capabilities;
|
||||
window.feedBackViz_highway_3d = () => ({});
|
||||
window.feedBackViz_piano = () => ({});
|
||||
|
||||
const settings = [
|
||||
{ key: 'palette', label: 'Palette', type: 'select', default: 'default',
|
||||
@@ -95,7 +95,7 @@ test('refreshProviders surfaces declared per-instance settings in the snapshot',
|
||||
// normalizes them, and the host reads them back from the participant.
|
||||
caps.registerParticipant('highway_3d', { visualization: { roles: ['provider'], settings } });
|
||||
|
||||
window.slopsmith.vizDomain.refreshProviders([
|
||||
window.feedBack.vizDomain.refreshProviders([
|
||||
{ id: 'highway_3d', label: '3D Highway' },
|
||||
{ id: 'piano', label: 'Piano' }, // no settings declared
|
||||
]);
|
||||
@@ -115,7 +115,7 @@ test('refreshProviders surfaces declared per-instance settings in the snapshot',
|
||||
|
||||
// list-providers carries the descriptors for consuming hosts; providers
|
||||
// without a declared settings list omit the field entirely.
|
||||
const snapshot = window.slopsmith.vizDomain.snapshot();
|
||||
const snapshot = window.feedBack.vizDomain.snapshot();
|
||||
const h3d = snapshot.providers.find(p => p.id === 'highway_3d');
|
||||
// Value-equal (the host deep-clones, so compare plain values, not refs).
|
||||
assert.deepEqual(JSON.parse(JSON.stringify(h3d.settings)), settings);
|
||||
@@ -142,17 +142,17 @@ test('refreshProviders surfaces declared per-instance settings in the snapshot',
|
||||
|
||||
test('refreshProviders unregisters providers that disappeared', () => {
|
||||
const window = loadVisualization();
|
||||
const api = window.slopsmith.capabilities;
|
||||
window.slopsmithViz_gone = () => ({});
|
||||
window.slopsmith.vizDomain.refreshProviders([{ id: 'gone', label: 'Gone' }]);
|
||||
const api = window.feedBack.capabilities;
|
||||
window.feedBackViz_gone = () => ({});
|
||||
window.feedBack.vizDomain.refreshProviders([{ id: 'gone', label: 'Gone' }]);
|
||||
assert.ok(api.inspect('visualization').participants.some(p => p.pluginId === 'gone'));
|
||||
window.slopsmith.vizDomain.refreshProviders([]);
|
||||
window.feedBack.vizDomain.refreshProviders([]);
|
||||
assert.ok(!api.inspect('visualization').participants.some(p => p.pluginId === 'gone'));
|
||||
});
|
||||
|
||||
test('select-renderer degrades on unknown provider and missing picker surface', async () => {
|
||||
const window = loadVisualization();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
const unknown = await api.dispatch({
|
||||
capability: 'visualization', command: 'select-renderer',
|
||||
source: 'test', payload: { providerId: 'nope' },
|
||||
@@ -160,8 +160,8 @@ test('select-renderer degrades on unknown provider and missing picker surface',
|
||||
assert.equal(unknown.outcome, 'degraded');
|
||||
assert.match(unknown.reason, /Unknown visualization provider/);
|
||||
|
||||
window.slopsmithViz_piano = () => ({});
|
||||
window.slopsmith.vizDomain.refreshProviders([{ id: 'piano', label: 'Piano' }]);
|
||||
window.feedBackViz_piano = () => ({});
|
||||
window.feedBack.vizDomain.refreshProviders([{ id: 'piano', label: 'Piano' }]);
|
||||
const noSurface = await api.dispatch({
|
||||
capability: 'visualization', command: 'select-renderer',
|
||||
source: 'test', payload: { providerId: 'piano' },
|
||||
@@ -172,11 +172,11 @@ test('select-renderer degrades on unknown provider and missing picker surface',
|
||||
|
||||
test('select-renderer and clear-renderer delegate to the app picker', async () => {
|
||||
const window = loadVisualization();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
const calls = [];
|
||||
window.setViz = (id) => calls.push(id);
|
||||
window.slopsmithViz_piano = () => ({});
|
||||
window.slopsmith.vizDomain.refreshProviders([{ id: 'piano', label: 'Piano' }]);
|
||||
window.feedBackViz_piano = () => ({});
|
||||
window.feedBack.vizDomain.refreshProviders([{ id: 'piano', label: 'Piano' }]);
|
||||
|
||||
const select = await api.dispatch({
|
||||
capability: 'visualization', command: 'select-renderer',
|
||||
@@ -192,15 +192,15 @@ test('select-renderer and clear-renderer delegate to the app picker', async () =
|
||||
|
||||
test('renderer change and failure are emitted as domain events', () => {
|
||||
const window = loadVisualization();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
const events = captureEvents(api, [
|
||||
'visualization:renderer-changed',
|
||||
'visualization:renderer-failed',
|
||||
]);
|
||||
|
||||
window.slopsmith.vizDomain.notifyRendererChanged('highway_3d', 'auto-match');
|
||||
window.slopsmith.vizDomain.notifyRendererChanged('highway_3d', 'auto-match'); // same id — no event
|
||||
window.slopsmith.vizDomain.notifyRendererFailed('highway_3d', 'init threw');
|
||||
window.feedBack.vizDomain.notifyRendererChanged('highway_3d', 'auto-match');
|
||||
window.feedBack.vizDomain.notifyRendererChanged('highway_3d', 'auto-match'); // same id — no event
|
||||
window.feedBack.vizDomain.notifyRendererFailed('highway_3d', 'init threw');
|
||||
|
||||
const changed = events.filter(e => e.event === 'renderer-changed');
|
||||
assert.equal(changed.length, 1);
|
||||
@@ -215,9 +215,9 @@ test('renderer change and failure are emitted as domain events', () => {
|
||||
|
||||
test('legacy shim accounting appears in the diagnostics snapshot', () => {
|
||||
const window = loadVisualization();
|
||||
const api = window.slopsmith.capabilities;
|
||||
window.slopsmithViz_piano = () => ({});
|
||||
window.slopsmith.vizDomain.refreshProviders([{ id: 'piano', label: 'Piano' }]);
|
||||
const api = window.feedBack.capabilities;
|
||||
window.feedBackViz_piano = () => ({});
|
||||
window.feedBack.vizDomain.refreshProviders([{ id: 'piano', label: 'Piano' }]);
|
||||
|
||||
const snapshot = api.snapshotDiagnostics();
|
||||
const shims = (snapshot.compatibilityShims || []).filter(s => s.capability === 'visualization');
|
||||
@@ -226,19 +226,19 @@ test('legacy shim accounting appears in the diagnostics snapshot', () => {
|
||||
const ids = JSON.parse(JSON.stringify(shims.map(s => s.shimId).sort()));
|
||||
assert.deepEqual(ids, [
|
||||
'visualization:type-visualization-manifest',
|
||||
'visualization:window.slopsmithViz_*',
|
||||
'visualization:window.feedBackViz_*',
|
||||
]);
|
||||
const windowShim = shims.find(s => s.shimId === 'visualization:window.slopsmithViz_*');
|
||||
const windowShim = shims.find(s => s.shimId === 'visualization:window.feedBackViz_*');
|
||||
assert.equal(windowShim.status, 'used');
|
||||
assert.ok(windowShim.hitCount >= 1);
|
||||
});
|
||||
|
||||
test('diagnostics contribution is redaction-safe (no song identity)', () => {
|
||||
const window = loadVisualization();
|
||||
window.slopsmith.vizDomain.noteAutoMatch('piano', true);
|
||||
window.slopsmith.vizDomain.notifyRendererFailed('piano', 'draw threw');
|
||||
window.feedBack.vizDomain.noteAutoMatch('piano', true);
|
||||
window.feedBack.vizDomain.notifyRendererFailed('piano', 'draw threw');
|
||||
const contribution = window.__diagnosticsContributions.get('visualization-capability');
|
||||
assert.equal(contribution.schema, 'slopsmith.visualization_capability.v1');
|
||||
assert.equal(contribution.schema, 'feedBack.visualization_capability.v1');
|
||||
const serialized = JSON.stringify(contribution);
|
||||
assert.ok(!/filename|title|artist|\.sloppak|\.archive/i.test(serialized), serialized);
|
||||
assert.deepEqual(
|
||||
@@ -251,7 +251,7 @@ test('a still-reserved future domain rejects dispatch', async () => {
|
||||
// note-detection was promoted by the spec-009 slice; backend.routes is
|
||||
// the canary that the reserved list still guards unpromoted domains.
|
||||
const window = loadVisualization();
|
||||
const api = window.slopsmith.capabilities;
|
||||
const api = window.feedBack.capabilities;
|
||||
const result = await api.dispatch({ capability: 'backend.routes', command: 'inspect', source: 'test' });
|
||||
assert.notEqual(result.outcome, 'handled');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user