const assert = require("assert"); const fs = require("fs"); const os = require("os"); const path = require("path"); const { EventEmitter } = require("events"); const { PassThrough } = require("stream"); const { DevelopmentUpdateService } = require("../src/services/development-updates"); const { isStrictLoopbackRequest, resolveRuntimeEnvironment } = require("../src/services/runtime-environment"); const { sameLocalhostOrigin } = require("../plugins/lumi_transcription/backend/companion/device_store"); async function main() { const companionManifest = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "plugins", "lumi_transcription", "companion_manifest.json"), "utf8")); const transcriptionManifest = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "plugins", "lumi_transcription", "plugin.json"), "utf8")); const companionProject = fs.readFileSync(path.join(__dirname, "..", "companion", "src", "Lumi.Companion.App", "Lumi.Companion.App.csproj"), "utf8"); const installerSource = fs.readFileSync(path.join(__dirname, "..", "companion", "installer", "Lumi.Companion.iss"), "utf8"); const publisherSource = fs.readFileSync(path.join(__dirname, "..", "companion", "scripts", "publish-companion.ps1"), "utf8"); const companionAppSource = fs.readFileSync(path.join(__dirname, "..", "companion", "src", "Lumi.Companion.App", "App.axaml.cs"), "utf8"); const iconFactorySource = fs.readFileSync(path.join(__dirname, "..", "companion", "src", "Lumi.Companion.App", "LumiIconFactory.cs"), "utf8"); const iconPath = path.join(__dirname, "..", "companion", "src", "Lumi.Companion.App", "Assets", "Lumi.Companion.ico"); const projectVersion = /([^<]+)<\/Version>/.exec(companionProject)?.[1]; const installerVersion = /#define AppVersion "([^"]+)"/.exec(installerSource)?.[1]; const publisherVersion = /\[string\]\$Version = "([^"]+)"/.exec(publisherSource)?.[1]; assert.equal(companionManifest.version, projectVersion, "the published Companion manifest and executable version must match"); assert.equal(transcriptionManifest.version, projectVersion, "the transcription plugin and Companion release version must match"); assert.equal(installerVersion, projectVersion, "the installer default version must match the Companion executable"); assert.equal(publisherVersion, projectVersion, "the release publisher default version must match the Companion executable"); assert(companionProject.includes("Assets\\Lumi.Companion.ico")); assert(companionProject.includes(' entry.id === "companion:core")); assert( first.components.find((entry) => entry.id === "companion:core").paths.includes("companion/scripts/publish-dev-update.ps1"), "changes to the localhost publisher must invalidate the cached Companion artifact" ); assert(first.components.some((entry) => entry.id === "companion-plugin:Song")); assert(first.components.some((entry) => entry.id === "lumi-plugin:now_playing")); const firstMap = Object.fromEntries(first.updatable_components.map((entry) => [entry.id, entry.checksum])); assert.equal(service.compare({ build_checksum: first.aggregate_checksum, component_checksums: firstMap }).update_available, false); fs.writeFileSync(path.join(temp, "companion", "plugins", "Song", "Plugin.cs"), "song-b"); const second = service.compare({ build_checksum: first.aggregate_checksum, component_checksums: firstMap }); assert.equal(second.update_available, true); assert.deepEqual(second.changed_components.map((entry) => entry.id), ["companion-plugin:Song"]); fs.mkdirSync(path.join(temp, "companion", "plugins", "Song", "bin"), { recursive: true }); fs.writeFileSync(path.join(temp, "companion", "plugins", "Song", "bin", "ignored.dll"), "volatile"); const third = service.inventory(); assert.equal(third.aggregate_checksum, second.aggregate_checksum); const beforeServerChange = third.aggregate_checksum; const beforeServerChecksum = third.components.find((entry) => entry.id === "lumi-plugin:now_playing").checksum; fs.writeFileSync(path.join(temp, "plugins", "now_playing", "index.js"), "server-b"); const fourth = service.compare({ build_checksum: beforeServerChange, component_checksums: Object.fromEntries(third.tracked_components.map((entry) => [entry.id, entry.checksum])) }); assert.notEqual(fourth.aggregate_checksum, beforeServerChange, "a Companion-related Lumi plugin change must be surfaced to the Companion"); assert.notEqual(fourth.components.find((entry) => entry.id === "lumi-plugin:now_playing").checksum, beforeServerChecksum); assert(fourth.changed_components.some((entry) => entry.id === "lumi-plugin:now_playing")); const fakeSpawn = (_command, args) => { const child = new EventEmitter(); child.stdout = new PassThrough(); child.stderr = new PassThrough(); child.kill = () => child.emit("exit", 1); setImmediate(() => { const outputIndex = args.indexOf("-OutputArchive"); const output = args[outputIndex + 1]; fs.mkdirSync(path.dirname(output), { recursive: true }); fs.writeFileSync(output, "fake-local-development-archive"); child.emit("exit", 0); }); return child; }; const buildService = new DevelopmentUpdateService({ repoRoot: temp, cacheRoot: path.join(temp, "data", "development-updates"), runtime: sourceMode, spawn: fakeSpawn, platform: "win32" }); const built = await buildService.buildCompanionArtifact(fourth); assert.equal(built.build_id, fourth.aggregate_checksum); assert.equal(buildService.readArtifact(built.build_id).sha256, built.sha256); assert.equal((await buildService.buildCompanionArtifact(fourth)).path, built.path, "the checksum-addressed build should be reused"); const serviceSource = fs.readFileSync(path.join(__dirname, "..", "src", "services", "development-updates.js"), "utf8"); assert(!/exec(?:File|Sync)?\([^\n]*git\b/i.test(serviceSource), "local development updates must not invoke Git"); const transcriptionSource = fs.readFileSync(path.join(__dirname, "..", "plugins", "lumi_transcription", "index.js"), "utf8"); assert(transcriptionSource.includes("allowsLocalDevelopmentUpdates")); assert(transcriptionSource.includes("dev-artifact/:buildId")); const updateSource = fs.readFileSync(path.join(__dirname, "..", "companion", "src", "Lumi.Companion.App", "UpdateService.cs"), "utf8"); assert(updateSource.includes("DevelopmentBuildManifest.Load()")); assert(updateSource.includes("update.Development")); assert(updateSource.includes("SameOrigin")); assert(updateSource.includes("SupportsPendingBuild")); assert(updateSource.includes("BuildPending")); const runtimeSource = fs.readFileSync(path.join(__dirname, "..", "companion", "src", "Lumi.Companion.App", "CompanionRuntime.cs"), "utf8"); assert(runtimeSource.includes("UpdateCheckTimeout = TimeSpan.FromMinutes(11)")); assert(transcriptionSource.includes("supports_pending_build")); assert(transcriptionSource.includes("build_pending")); const applierSource = fs.readFileSync(path.join(__dirname, "..", "companion", "src", "Lumi.Companion.App", "UpdateApplier.cs"), "utf8"); assert(applierSource.includes(".lumi-dev-build.json")); console.log("Local development update verification passed: global runtime mode, strict loopback gating, per-plugin checksums, ignored build output, Companion-related server-plugin synchronization, Git-free comparison, and Companion same-version update flow."); } finally { fs.rmSync(temp, { recursive: true, force: true }); } } main().catch((error) => { console.error(error); process.exitCode = 1; });