const assert = require("assert"); const fs = require("fs"); const path = require("path"); const root = path.resolve(__dirname, ".."); const repoRoot = path.resolve(root, "..", ".."); const plugin = require(path.join(root, "index.js")); const test = plugin._test; assert(test, "Plugin test helpers are unavailable."); const incoming = test.normalizeIncomingEvent({ protocol_version: 1, provider: "spotify", device_id: "device", session_id: "session", sequence: 1, event: "track_changed", occurred_at: Date.now(), playback: { status: "playing", position_ms: 2000, duration_ms: 180000, rate: 1 }, track: { key: "song", title: "Song", artist: "Artist", album: "Album", release_year: "2026", link: "https://example.com/song" } }); assert.equal(incoming.playback.position_ms, 2000); assert.equal(incoming.playback.duration_ms, 180000); assert.equal(incoming.track.title, "Song"); const state = test.applyIncomingEvent(null, incoming, null); assert.equal(state.track_key, "song"); assert.equal(state.playback_status, "playing"); assert.deepEqual(test.announcementTokens(state), { song_name: "Song", artist: "Artist", album: "Album", release_year: "2026", link: "https://example.com/song" }); const paused = test.applyIncomingEvent(state, test.normalizeIncomingEvent({ protocol_version: 1, provider: "spotify", device_id: "device", session_id: "session", sequence: 2, event: "pause", playback: { status: "paused", position_ms: 8000, duration_ms: 180000, rate: 1 } }), null); assert.equal(paused.track_key, "song", "Track metadata should survive a status-only update."); assert.equal(paused.position_ms, 8000); const stopped = test.applyIncomingEvent(paused, test.normalizeIncomingEvent({ protocol_version: 1, provider: "spotify", device_id: "device", session_id: "session", sequence: 3, event: "stop", playback: { status: "stopped", position_ms: 0, duration_ms: 0, rate: 1 } }), null); assert.equal(stopped.track_key, null, "A trackless stop event must clear the active song."); let delegated = false; const previousFrameworks = global.lumiFrameworks; global.lumiFrameworks = { companion: { requireDevice(req, _res, next) { delegated = req.marker === "shared-device-auth"; next(); } } }; let continued = false; test.requireCompanionDevice({ marker: "shared-device-auth" }, {}, () => { continued = true; }); assert(delegated && continued, "Song Overlay must delegate authentication to the shared Companion device framework."); global.lumiFrameworks = {}; let unavailableStatus = 0; test.requireCompanionDevice({}, { status(code) { unavailableStatus = code; return this; }, json() { return this; } }, () => assert.fail("A missing Companion authentication framework must not allow the request.")); assert.equal(unavailableStatus, 503); global.lumiFrameworks = previousFrameworks; assert.equal( test.withAuthenticatedDevice({ lumiDevice: { id: "paired-device" } }, { device_id: "spoofed-device" }).device_id, "paired-device", "Song Overlay state must use the authenticated Companion identity rather than a plugin-supplied device ID." ); for (const file of ["views/admin.ejs", "views/render.ejs", "public/admin.js", "public/render.js", "public/admin.css", "public/render.css"]) { assert(fs.existsSync(path.join(root, file)), `Missing ${file}`); } const serverSource = fs.readFileSync(path.join(root, "index.js"), "utf8"); const adminSource = fs.readFileSync(path.join(root, "views", "admin.ejs"), "utf8"); const renderSource = fs.readFileSync(path.join(root, "public", "render.js"), "utf8"); const renderStyles = fs.readFileSync(path.join(root, "public", "render.css"), "utf8"); const transportContract = fs.readFileSync(path.join(repoRoot, "companion", "src", "Lumi.Companion.Abstractions", "CompanionPluginTransport.cs"), "utf8"); const hostTransport = fs.readFileSync(path.join(repoRoot, "companion", "src", "Lumi.Companion.Core", "CompanionPluginTransport.cs"), "utf8"); const songRuntime = fs.readFileSync(path.join(repoRoot, "companion", "plugins", "Lumi.Companion.SongOverlay", "SongOverlayRuntime.cs"), "utf8"); const songSettings = fs.readFileSync(path.join(repoRoot, "companion", "plugins", "Lumi.Companion.SongOverlay", "SongOverlaySettings.cs"), "utf8"); const companionUi = fs.readFileSync(path.join(repoRoot, "companion", "src", "Lumi.Companion.App", "MainWindow.axaml"), "utf8"); const transcriptionSource = fs.readFileSync(path.join(repoRoot, "plugins", "lumi_transcription", "index.js"), "utf8"); assert(transportContract.includes("ICompanionPluginTransport")); assert(hostTransport.includes('new AuthenticationHeaderValue(') && hostTransport.includes('"LumiDevice"')); assert(hostTransport.includes('expectedPrefix = $"/plugins/{_pluginId}/"'), "the host transport must scope plugins to their own server route"); assert(songRuntime.includes("ICompanionPluginTransport") && !songRuntime.includes("SetConnectionKey")); assert(!songSettings.includes("ProtectedConnectionKey") && !songSettings.includes("LumiBaseUrl")); assert(!songSettings.includes("DeviceId")); assert(!companionUi.includes("SongOverlayConnectionKeyBox") && !companionUi.includes("SongOverlayHostBox")); assert(!serverSource.includes('router.post("/connection-key"') && !serverSource.includes("connection_token_hash")); assert(!adminSource.includes("Generate connection key")); assert(transcriptionSource.includes("global.lumiFrameworks.companion = companionFramework")); assert(serverSource.includes('triggers: ["music"]'), "Song Overlay must register the !music command with Lumi's command router."); assert( serverSource.includes("renderAnnouncementMessage({ db, state, user: ctx?.user })") && serverSource.includes("renderAnnouncementMessage({ db, state, user: reqUser })"), "!music and automatic announcements must share one announcement formatter." ); assert(serverSource.includes('"Access-Control-Allow-Origin": "*"'), "the sandboxed overlay must be able to connect to its token event stream."); assert(serverSource.includes('writeSse(client.res, "changed"'), "overlay updates should signal clients to re-fetch canonical state."); assert(renderSource.includes('addEventListener("changed", refreshState)')); assert(renderSource.includes('addEventListener("error", startFallbackPolling)'), "overlay updates need a fallback when SSE is interrupted."); assert(renderStyles.includes("color-scheme: normal") && !renderStyles.includes("color-scheme: dark")); assert(renderStyles.includes("background-color: rgba(0, 0, 0, 0) !important"), "the rendered overlay canvas must be transparent."); try { const ejs = require("ejs"); for (const file of ["views/admin.ejs", "views/render.ejs"]) ejs.compile(fs.readFileSync(path.join(root, file), "utf8"), { filename: path.join(root, file) }); console.log("EJS templates compiled."); } catch (error) { if (error.code === "MODULE_NOT_FOUND") console.warn("EJS is not installed in this checkout; template compilation was skipped."); else throw error; } console.log("Song Overlay plugin verification passed.");