feedBack/tests/test_plugin_runtime_idempotence.py
Bret Mogilefsky af2949677a
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>
2026-06-23 11:03:01 +02:00

185 lines
9.5 KiB
Python

from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[1]
WORKSPACE_ROOT = ROOT.parent
def _sibling_file(plugin_dir: str, filename: str) -> Path:
path = WORKSPACE_ROOT / plugin_dir / filename
if not path.exists():
pytest.skip(f"requires sibling plugin checkout: {plugin_dir}/{filename}")
return path
def _sibling_text(plugin_dir: str, filename: str, required_token: str | None = None) -> str:
text = _sibling_file(plugin_dir, filename).read_text(encoding="utf-8")
if required_token and required_token not in text:
pytest.skip(f"requires {plugin_dir} checkout with {required_token}")
return text
def test_plugin_loader_guards_duplicate_hydration_and_scripts():
source = (ROOT / "static" / "app.js").read_text(encoding="utf-8")
assert "let _loadPluginsInFlight = false" in source
assert "window.feedBack._loadedPluginScripts" in source
assert "document.querySelectorAll('.screen[id^=\"plugin-\"]')" in source
def test_plugin_loader_unmounts_previous_ui_contributions_before_reregistering():
source = (ROOT / "static" / "app.js").read_text(encoding="utf-8")
assert "const _pluginUiContributions = new Map()" in source
assert "await _commandUiDomain(contribution.domain, 'unmount', plugin, contribution)" in source
assert "await _commandUiDomain(contribution.domain, 'register-contribution', plugin, contribution)" in source
assert "await _commandUiDomain(contribution.domain, 'mount', plugin, contribution)" in source
def test_plugin_loader_unmounts_contributions_for_removed_plugins():
source = (ROOT / "static" / "app.js").read_text(encoding="utf-8")
assert "const livePluginIds = new Set(plugins.map((plugin) => plugin.id))" in source
assert "for (const [pluginId, contributions] of _pluginUiContributions)" in source
assert "const stalePlugin = { id: pluginId }" in source
assert "await _commandUiDomain(contribution.domain, 'unmount', stalePlugin, contribution)" in source
assert "window.feedBack?.capabilities?.unregisterParticipant?.(pluginId)" in source
assert "_pluginUiContributions.delete(pluginId)" in source
def test_capability_visualizer_waits_for_registry_instead_of_hard_error():
source = _sibling_file("feedBack-plugin-capability-visualizer", "screen.js").read_text(encoding="utf-8")
assert "scheduleRegistryRetry" in source
assert "Capability runtime is loading..." in source
assert "Capability registry unavailable" not in source
def test_app_shell_loads_capability_registry_before_app_runtime():
source = (ROOT / "static" / "index.html").read_text(encoding="utf-8")
assert '<script src="/static/capabilities.js"></script>' in source
assert '<script src="/static/capabilities/library.js"></script>' in source
assert source.index('/static/diagnostics.js') < source.index('/static/capabilities.js')
assert source.index('/static/capabilities.js') < source.index('/static/capabilities/library.js')
assert source.index('/static/capabilities/library.js') < source.index('/static/app.js')
def test_capability_registry_exposes_claim_dispatch_and_ready_contracts():
source = (ROOT / "static" / "capabilities.js").read_text(encoding="utf-8")
for token in ["function claim(", "function release(", "async function dispatch(", "function subscribe(", "getDiagnostics: snapshotDiagnostics"]:
assert token in source
assert "activeClaims" in source
assert "feedBack:capabilities:ready" in source
assert "outcome: 'overridden'" in source
def test_capability_runtime_overrides_do_not_mask_claims():
source = (ROOT / "static" / "capabilities.js").read_text(encoding="utf-8")
set_enabled = source[source.index("function setParticipantEnabled("):source.index("function registerParticipants(")]
reserved = source[source.index("const RESERVED_FUTURE_DOMAINS"):source.index("const RUNTIME_DOMAIN_DEFAULTS")]
assert "['denied', 'failed', 'short-circuited', 'handled', 'degraded', 'overridden', 'no-owner', 'no-handler', 'no-target', 'unsupported-command', 'incompatible', 'incompatible-version', 'unavailable', 'provider-selection-required', 'user-action-required', 'stale', 'cancelled', 'stopped'].includes(decision.outcome)" in source
assert "if (entry.type !== 'manual') return false;" in source
assert "type: 'manual'" in source
assert "_remember(userOverrides" not in set_enabled
assert "'audio-monitoring'" not in reserved
assert "'audio-mix'" not in reserved
assert "'audio-input'" not in reserved
assert "'playback'" not in reserved
assert "playback:" in source
assert "'backend.routes'" in reserved
assert "'backend.routes':" not in source
def test_deferred_runtime_domains_remain_reserved_not_bridged():
capability_source = (ROOT / "static" / "capabilities.js").read_text(encoding="utf-8")
reserved = capability_source[capability_source.index("const RESERVED_FUTURE_DOMAINS"):capability_source.index("const RUNTIME_DOMAIN_DEFAULTS")]
review = capability_source[capability_source.index("const CORE_DOMAIN_REVIEW"):capability_source.index("const EXPECTED_COMPATIBILITY_SHIMS")]
# Domains still reserved (not yet promoted).
for token in ["'ui.navigation'"]:
assert token in reserved, f"{token} should still be in RESERVED_FUTURE_DOMAINS"
assert token not in review, f"{token} should not yet appear in CORE_DOMAIN_REVIEW"
# 'visualization' was promoted to an active domain (cap:6).
assert "'visualization'" not in reserved, "'visualization' should have been removed from RESERVED_FUTURE_DOMAINS after promotion"
assert "visualization:" in review, "'visualization' should appear in CORE_DOMAIN_REVIEW after promotion"
# 'note-detection' was promoted to an active domain (cap:9 / spec 009).
assert "'note-detection'" not in reserved, "'note-detection' should have been removed from RESERVED_FUTURE_DOMAINS after promotion"
assert "note-detection':" in review, "'note-detection' should appear in CORE_DOMAIN_REVIEW after promotion"
assert "playback:" in review
assert "chartT: _chartTime(audioT)" not in capability_source
assert "loop: _loopSnapshot()" not in capability_source
def test_capability_events_do_not_bridge_deferred_surfaces():
app_source = (ROOT / "static" / "app.js").read_text(encoding="utf-8")
capability_source = (ROOT / "static" / "capabilities.js").read_text(encoding="utf-8")
for token in ["return 'ui.navigation'", "return 'note-detection'", "eventName.startsWith('viz:') || eventName.startsWith('highway:')"]:
assert token not in app_source
for token in ["'navigate'", "'screen:changed'", "function _navigate(", "window.feedBack.navigate(id, params)"]:
assert token not in capability_source
def test_plugin_loader_registers_manifest_capability_declarations():
source = (ROOT / "static" / "app.js").read_text(encoding="utf-8")
assert "const capabilityPlugins = fetchedPlugins.slice().sort((a, b) => String(a.id || '').localeCompare(String(b.id || '')))" in source
assert "capabilityApi.registerParticipants(capabilityPlugins)" in source
assert "window.feedBack.capabilities.registerParticipants(plugins)" not in source
assert "plugin-manifest-load" in source
def test_app_event_bus_dispatches_locally_and_preserves_juce_stop_state():
source = (ROOT / "static" / "app.js").read_text(encoding="utf-8")
assert "this.dispatchEvent(new CustomEvent(event, { detail }))" in source
assert "const hadPlayableSong = !!audio.src || !!window._juceAudioUrl || isPlaying" in source
assert "sm.emit('song:resume', payload)" in source
assert "window.feedBack.emit('song:resume', payload)" in source
def test_nam_and_stems_use_owner_claim_dispatch_semantics():
nam_source = _sibling_text("feedBack-plugin-nam-tone", "screen.js", "NAM_STEM_CLAIM_ID = 'nam.amp-active'")
stems_source = _sibling_text("feedBack-plugin-stems", "screen.js", "claimSnapshots")
assert "NAM_STEM_CLAIM_ID = 'nam.amp-active'" in nam_source
assert "api.dispatch({" in nam_source
assert "command: 'mute'" in nam_source
assert "command: 'restore'" in nam_source
assert "claim: { claimId, requester: NAM_PLUGIN_ID }" in nam_source
assert "window._stemsState" not in nam_source
assert "claimSnapshots" in stems_source
assert "api.registerParticipant('stems'" in stems_source
assert "mute: capMute" in stems_source
assert "restore: capRestore" in stems_source
assert "recordUserOverride" in stems_source
assert "clearClaimSnapshots" in stems_source
assert "'claim:released'" in stems_source
def test_nam_screen_uses_stable_singleton_hooks_for_rehydration():
source = _sibling_text("feedBack-plugin-nam-tone", "screen.js", "window.__feedBackNamHooks")
manifest = _sibling_text("feedBack-plugin-nam-tone", "plugin.json", "capability-pipelines.v1")
assert "plugin-runtime-idempotent.v1" in manifest
assert "capability-pipelines.v1" in manifest
assert "window.__feedBackNamHooks" in source
assert "hookState.impl" in source
assert "if (hookState.installed) return" in source
def test_stems_screen_uses_stable_singleton_hooks_for_rehydration():
source = _sibling_text("feedBack-plugin-stems", "screen.js", "window.__feedBackStemsHooks")
manifest = _sibling_text("feedBack-plugin-stems", "plugin.json", "capability-pipelines.v1")
assert "plugin-runtime-idempotent.v1" in manifest
assert "capability-pipelines.v1" in manifest
assert "window.__feedBackStemsHooks" in source
assert "hookState.impl" in source
assert "if (hookState.installed) return" in source