mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-07-22 12:52:29 +00:00
The first carve, and deliberately the riskiest: app.js IS the plugin loader (the
R0 host rails), so it goes first while the module graph is still one edge deep.
static/js/plugin-loader.js (829 lines) — bodies VERBATIM. app.js 12,217 → 11,439.
Core's first `static/js/` module, exactly as constitution II anticipates.
CLOSURE (measured with acorn, not regex — brace-matching stripped source drifted):
the block at app.js:11246-12031 is contiguous and self-contained. It needs only
TWO things from the rest of app.js, and exports only TWO:
exports: loadPlugins (the window contract), bootstrapPluginsAndUi (boot)
inbound: window.showScreen — already the public host contract (constitution II),
so it is called through `window`, not re-coupled as an import
_populateVizPicker — injected via configurePluginLoader()
WHY A SEAM, NOT AN IMPORT. plugin-loader must not import app.js: app.js imports
it, so that would close a cycle. I checked whether _populateVizPicker could just
move into the module instead (which would delete the seam entirely) — it drags 9
further symbols (_canRun3D, _autoMatchViz, _showPromotionNag, …), i.e. a whole
viz cluster. That is its own carve, so the seam stays.
THE SEAM'S DEFAULT IS LOUD, ON PURPOSE. A no-op stub is the classic silent
failure for this pattern (see the editor's setHostHooks trap, hit twice): drop the
wiring call and the loader keeps working while the viz picker quietly stops
refreshing — no test, no boot check says a word. The default now console.errors,
so the smoke harness catches it. VERIFIED BY BITE TEST: removing
configurePluginLoader() from app.js surfaces
"[plugin-loader] host seam not configured" at boot. The seam IS exercised on the
plugin-startup path, so an unwired hook cannot pass silently.
no-cycle is now LIVE on core's own graph for the first time. eslint.config.js
gains `static/app.js` + `static/js/**` to the module block — app.js now `import`s,
so parsing it as a script would be a syntax error. VERIFIED BY BITE TEST: making
plugin-loader import app.js back fails with "Dependency cycle detected".
HARNESSES (the R3a note said budget one conversion per carve — it was five):
retargeted capability_inspector_nav, plugin_hydration_wipe,
plugin_loader_script_type, plugin_style_injection, legacy_shim_hits (SPLIT — one
test needs the loader, one still needs app.js) + test_plugin_runtime_idempotence.
legacy_shim_hits was missed by a symbol-name grep because it greps for a code
STRING; only the failing run found it. test_capability_events' NEGATIVE asserts
now span app.js + the loader — carving code out of app.js would otherwise make
them vacuous instead of failing.
VERIFIED: A/B against origin/main in two browsers — mounted plugin screens, 14
loaded plugin scripts, the 3 module plugins injected as <script type="module">,
37 capability participants, 14 shims, window.loadPlugins: IDENTICAL, zero
console/page errors on both. /static/js/plugin-loader.js serves 200; R0 rails
intact (src/main.js 200, conditional GET 304, script_type passthrough).
pytest 2396, node 1032/1032, ESLint 0, Codex 0.
Codex preflight caught a REAL [P1] first pass: static/js/plugin-loader.js was
untracked, so a checkout would have served an app.js importing a nonexistent
module — a failed static import kills the whole module and every window handler
with it. Now tracked.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
116 lines
4.5 KiB
JavaScript
116 lines
4.5 KiB
JavaScript
// Verify loadPlugins' plugin-DOM wipe loops in static/js/plugin-loader.js: a plugin that is
|
|
// merely ABSENT from the current /api/plugins response (transient partial
|
|
// response while the backend's plugin registry is repopulating after a
|
|
// restart) must keep its settings panel and screen DOM. Wiping it while its
|
|
// _loadedPluginScripts entry survives made the next refetch fail the
|
|
// DOM-existence check and re-evaluate the plugin's screen.js mid-session —
|
|
// which duplicated the desktop audio_engine's native signal chain. Plugins
|
|
// the response knows about but that failed hydration are still wiped, as is
|
|
// junk DOM carrying no plugin id.
|
|
|
|
const { test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const vm = require('node:vm');
|
|
|
|
const PLUGIN_LOADER_JS = path.join(__dirname, '..', '..', 'static', 'js', 'plugin-loader.js');
|
|
|
|
// Slice the wipe block out of loadPlugins by its stable landmarks: from the
|
|
// nav reset that opens it to the comment introducing the next section.
|
|
function extractWipeBlock(src) {
|
|
const start = src.indexOf("navContainer.innerHTML = '';");
|
|
assert.ok(start !== -1, 'wipe block start (nav reset) not found');
|
|
const end = src.indexOf('// Plugin settings area hosts', start);
|
|
assert.ok(end !== -1, 'wipe block end marker not found');
|
|
return src.slice(start, end);
|
|
}
|
|
|
|
function makeEl(pluginId, id) {
|
|
return {
|
|
dataset: pluginId != null ? { pluginId } : {},
|
|
id: id || (pluginId != null ? `plugin-${pluginId}` : ''),
|
|
removed: false,
|
|
remove() {
|
|
this.removed = true;
|
|
const idx = this._parent ? this._parent.indexOf(this) : -1;
|
|
if (idx >= 0) this._parent.splice(idx, 1);
|
|
},
|
|
};
|
|
}
|
|
|
|
function runWipe({ respondedIds, alreadyHydrated, settingsChildren, screens }) {
|
|
const src = fs.readFileSync(PLUGIN_LOADER_JS, 'utf8');
|
|
const block = extractWipeBlock(src);
|
|
settingsChildren.forEach((el) => { el._parent = settingsChildren; });
|
|
const container = { children: settingsChildren };
|
|
const sandbox = {
|
|
navContainer: { innerHTML: 'seed' },
|
|
mobileNavContainer: { innerHTML: 'seed' },
|
|
_pluginSettingsContainers: () => [container],
|
|
respondedIds,
|
|
alreadyHydrated,
|
|
document: {
|
|
querySelectorAll: (sel) => {
|
|
assert.equal(sel, '.screen[id^="plugin-"]');
|
|
return screens.slice();
|
|
},
|
|
},
|
|
};
|
|
vm.runInNewContext(block, sandbox, { filename: 'wipe-block.js' });
|
|
return sandbox;
|
|
}
|
|
|
|
test('plugin absent from the response keeps its settings + screen DOM', () => {
|
|
const settings = makeEl('audio_engine');
|
|
const screen = makeEl('audio_engine');
|
|
runWipe({
|
|
respondedIds: new Set(), // partial response: plugin missing
|
|
alreadyHydrated: new Set(), // scan loop never saw it either
|
|
settingsChildren: [settings],
|
|
screens: [screen],
|
|
});
|
|
assert.equal(settings.removed, false, 'settings panel must survive a partial response');
|
|
assert.equal(screen.removed, false, 'screen must survive a partial response');
|
|
});
|
|
|
|
test('plugin present in the response but not hydrated is wiped', () => {
|
|
const settings = makeEl('stale_plugin');
|
|
const screen = makeEl('stale_plugin');
|
|
runWipe({
|
|
respondedIds: new Set(['stale_plugin']),
|
|
alreadyHydrated: new Set(),
|
|
settingsChildren: [settings],
|
|
screens: [screen],
|
|
});
|
|
assert.equal(settings.removed, true);
|
|
assert.equal(screen.removed, true);
|
|
});
|
|
|
|
test('hydrated plugin present in the response is preserved', () => {
|
|
const settings = makeEl('audio_engine');
|
|
const screen = makeEl('audio_engine');
|
|
runWipe({
|
|
respondedIds: new Set(['audio_engine']),
|
|
alreadyHydrated: new Set(['audio_engine']),
|
|
settingsChildren: [settings],
|
|
screens: [screen],
|
|
});
|
|
assert.equal(settings.removed, false);
|
|
assert.equal(screen.removed, false);
|
|
});
|
|
|
|
test('junk DOM without a plugin id is still removed', () => {
|
|
const junkSettings = makeEl(null);
|
|
// Screen whose id strips to '' (no dataset.pluginId, bare "plugin-" id).
|
|
const junkScreen = makeEl(null, 'plugin-');
|
|
runWipe({
|
|
respondedIds: new Set(['whatever']),
|
|
alreadyHydrated: new Set(),
|
|
settingsChildren: [junkSettings],
|
|
screens: [junkScreen],
|
|
});
|
|
assert.equal(junkSettings.removed, true);
|
|
assert.equal(junkScreen.removed, true);
|
|
});
|