From 55939858842eed0bc328ea68de6cf4ca565fb8b6 Mon Sep 17 00:00:00 2001 From: Simeon Radivoev Date: Fri, 15 May 2026 15:07:51 +0300 Subject: [PATCH] chore: Fixed tests --- .gitignore | 1 + src/bun/api/app.ts | 7 ++ src/bun/api/games/games.ts | 6 +- .../jobs/{update-store.ts => ensure-store.ts} | 27 +++--- src/bun/api/jobs/jobs.ts | 4 +- .../store.ts | 10 +-- src/bun/api/plugins/register-plugins.ts | 89 ++++++++++--------- src/bun/api/plugins/services.ts | 2 + src/bun/api/store/store.ts | 6 +- src/bun/utils.ts | 15 ++++ src/packages/gameflow-sdk/index.ts | 3 +- src/packages/gameflow-sdk/task-queue.ts | 29 ++++++ src/tests/preload.ts | 6 +- 13 files changed, 139 insertions(+), 66 deletions(-) rename src/bun/api/jobs/{update-store.ts => ensure-store.ts} (60%) diff --git a/.gitignore b/.gitignore index e7e5c74..880e27d 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,7 @@ downloads gameflow-deck.code-workspace .env.local src/tests/mock-roms/db.sqlite +src/tests/mock-roms/store src/tests/mock-config bin .config/flatpak/repo diff --git a/src/bun/api/app.ts b/src/bun/api/app.ts index 68ec287..4695726 100644 --- a/src/bun/api/app.ts +++ b/src/bun/api/app.ts @@ -116,6 +116,13 @@ export async function cleanup () cleannedUp = true; } +/** Reset the cleanup flags. This is mainly used by tests since they run the same app. */ +export async function resetCleanup () +{ + cleaningUp = false; + cleannedUp = false; +} + export async function reloadDatabase () { await ensureDir(config.get('downloadPath')); diff --git a/src/bun/api/games/games.ts b/src/bun/api/games/games.ts index f06f71f..d922b36 100644 --- a/src/bun/api/games/games.ts +++ b/src/bun/api/games/games.ts @@ -454,18 +454,18 @@ export default new Elysia() }, { params: z.object({ id: z.string(), source: z.string() }), }) - .post('/game/:source/:id/install', async ({ params: { id, source }, body: { downloadId } }) => + .post('/game/:source/:id/install', async ({ params: { id, source }, body }) => { if (!taskQueue.findJob(InstallJob.query({ source, id }), InstallJob)) { - return taskQueue.enqueue(InstallJob.query({ source, id }), new InstallJob(id, source, { downloadId })); + return taskQueue.enqueue(InstallJob.query({ source, id }), new InstallJob(id, source, body)); } else { return status('Not Implemented'); } }, { params: z.object({ id: z.string(), source: z.string() }), - body: z.object({ downloadId: z.string().optional() }), + body: z.object({ downloadId: z.string().optional() }).optional(), response: z.any() }) .delete('/game/:source/:id/install', async ({ params: { id, source } }) => diff --git a/src/bun/api/jobs/update-store.ts b/src/bun/api/jobs/ensure-store.ts similarity index 60% rename from src/bun/api/jobs/update-store.ts rename to src/bun/api/jobs/ensure-store.ts index 697fb3a..bce028b 100644 --- a/src/bun/api/jobs/update-store.ts +++ b/src/bun/api/jobs/ensure-store.ts @@ -6,8 +6,9 @@ import { runBunPackageCommand } from "../plugins/services"; import { PluginRegistry } from "@/shared/constants"; import path from "node:path"; import sdkPkg from '@simeonradivoev/gameflow-sdk/package.json'; +import { IsPluginAllowed } from "@/bun/utils"; -export default class UpdateStoreJob implements IJob +export default class EnsureStore implements IJob { static id = "update-store" as const; static dataSchema = z.never(); @@ -20,7 +21,7 @@ export default class UpdateStoreJob implements IJob this.storeVersion = process.env.STORE_VERSION ?? "^0.1.0"; } - async start (context: JobContext) + async start (context: JobContext) { const storeFolder = getStoreRootFolder(); await ensureDir(storeFolder); @@ -32,17 +33,23 @@ export default class UpdateStoreJob implements IJob const storePackage = await Bun.file(path.join(storeFolder, "package.json")).json(); - if (!storePackage.dependencies?.[sdkPkg.name] || storePackage.dependencies?.[sdkPkg.name] !== sdkPkg.version) + if (IsPluginAllowed(sdkPkg.name)) { - let response = await runBunPackageCommand(["add", `${sdkPkg.name}@${sdkPkg.version}`, "--registry", PluginRegistry, '--omit', 'peer']); - console.log(response); - } + if (!storePackage.dependencies?.[sdkPkg.name] || storePackage.dependencies?.[sdkPkg.name] !== sdkPkg.version) + { + let response = await runBunPackageCommand(["add", `${sdkPkg.name}@${sdkPkg.version}`, "--registry", PluginRegistry, '--omit', 'peer']); + console.log(response); + } - // probably just means we couldn't find a version of the sdk, just install latest - if (storePackage.dependencies?.[sdkPkg.name] !== sdkPkg.version) + // probably just means we couldn't find a version of the sdk, just install latest + if (storePackage.dependencies?.[sdkPkg.name] !== sdkPkg.version) + { + let response = await runBunPackageCommand(["add", '--exact', `${sdkPkg.name}@latest`, "--registry", PluginRegistry, '--omit', 'peer']); + console.log(response); + } + } else { - let response = await runBunPackageCommand(["add", '--exact', `${sdkPkg.name}@latest`, "--registry", PluginRegistry, '--omit', 'peer']); - console.log(response); + console.log("Ignoring SDK package"); } if (process.env.CUSTOM_STORE_PATH) return; diff --git a/src/bun/api/jobs/jobs.ts b/src/bun/api/jobs/jobs.ts index e7e20a2..5471e56 100644 --- a/src/bun/api/jobs/jobs.ts +++ b/src/bun/api/jobs/jobs.ts @@ -3,7 +3,7 @@ import z, { _ZodType } from "zod"; import { taskQueue } from "../app"; import { LoginJob } from "./login-job"; import TwitchLoginJob from "./twitch-login-job"; -import UpdateStoreJob from "./update-store"; +import EnsureStore from "./ensure-store"; import { EmulatorDownloadJob } from "./emulator-download-job"; import { getErrorMessage } from "@/bun/utils"; import { BaseEvent, IJob } from "@simeonradivoev/gameflow-sdk/task-queue"; @@ -184,7 +184,7 @@ export const jobs = new Elysia({ prefix: '/api/jobs' }) .use(registerJob(LaunchGameJob)) .use(registerJob(LoginJob)) .use(registerJob(TwitchLoginJob)) - .use(registerJob(UpdateStoreJob)) + .use(registerJob(EnsureStore)) .use(registerJob(BiosDownloadJob)) .use(registerJob(InstallJob)) .use(registerJob(ReloadPluginsJob)) diff --git a/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.store/store.ts b/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.store/store.ts index 118eb11..90d1cbc 100644 --- a/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.store/store.ts +++ b/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.store/store.ts @@ -2,14 +2,14 @@ import { PluginLoadingContextType, PluginType } from "@simeonradivoev/gameflow-s import desc from './package.json'; import path, { } from 'node:path'; import { buildStoreFrontendEmulatorSystems, getAllStoreEmulatorPackages, getStoreEmulatorPackage, getStoreFolder } from "@/bun/api/store/services/gamesService"; -import { Glob, pathToFileURL, which } from "bun"; +import { Glob, pathToFileURL, sleep, which } from "bun"; import { and, eq } from "drizzle-orm"; import * as emulatorSchema from '@schema/emulators'; import { config, emulatorsDb, taskQueue } from "@/bun/api/app"; import fs from "node:fs/promises"; import { getSourceGameDetailed } from "@/bun/api/games/services/utils"; -import UpdateStoreJob from "@/bun/api/jobs/update-store"; +import EnsureStore from "@/bun/api/jobs/ensure-store"; import { getEmulatorDownload, getEmulatorPath } from "@/bun/api/store/services/emulatorsService"; import { buildFilters, buildLaunchCommand, buildSaves, convertStoreEmulatorToFrontend, convertStoreToFrontend, convertStoreToFrontendDetailed, getExistingStoreEmulatorDownload, getShuffledStoreGames, getStoreGame, getValidDownloads } from "./services"; import { DownloadInfo, FrontEndEmulatorDetailed, FrontEndGameTypeWithIds } from "@simeonradivoev/gameflow-sdk/shared"; @@ -20,7 +20,7 @@ import StreamZip from "node-stream-zip"; import { path7za } from "7zip-bin"; import Seven from 'node-7z'; -export default class RommIntegration implements PluginType +export default class StoreIntegration implements PluginType { eventsNames = [{ id: 'updateStore', title: "Update Store", description: "Update the Store Manifest", action: "Update" }]; @@ -29,7 +29,7 @@ export default class RommIntegration implements PluginType switch (e) { case 'updateStore': - await taskQueue.enqueue(UpdateStoreJob.id, new UpdateStoreJob()); + await taskQueue.enqueue(EnsureStore.id, new EnsureStore()); return { reload: true }; } } @@ -38,7 +38,7 @@ export default class RommIntegration implements PluginType { console.log("Store Directory is ", getStoreFolder()); ctx.setProgress(0, "Updating Store"); - await taskQueue.enqueue(UpdateStoreJob.id, new UpdateStoreJob()); + await taskQueue.enqueue(EnsureStore.id, new EnsureStore()); } async load (ctx: PluginLoadingContextType) diff --git a/src/bun/api/plugins/register-plugins.ts b/src/bun/api/plugins/register-plugins.ts index 1275740..a4b5666 100644 --- a/src/bun/api/plugins/register-plugins.ts +++ b/src/bun/api/plugins/register-plugins.ts @@ -14,10 +14,12 @@ import rclone from './builtin/other/com.simeonradivoev.gameflow.rclone/package.j import { PluginDescriptionSchema, PluginDescriptionType, PluginSchema } from "@simeonradivoev/gameflow-sdk"; import path from 'node:path'; import { getStoreRootFolder } from "../store/services/gamesService"; -import { getUpdates } from "./services"; +import { getUpdates, runBunPackageCommand } from "./services"; import { PluginSourceType } from "@simeonradivoev/gameflow-sdk/shared"; import { taskQueue } from "../app"; -import UpdateStoreJob from "../jobs/update-store"; +import EnsureStore from "../jobs/ensure-store"; +import { PluginRegistry } from "@/shared/constants"; +import { IsPluginAllowed } from "@/bun/utils"; type PluginEntry = PluginDescriptionType & { load: () => Promise; }; @@ -58,15 +60,9 @@ export async function unregisterPlugin (id: string, pluginManager: PluginManager export async function registerPlugin (plugin: PluginEntry, source: PluginSourceType, pluginManager: PluginManager) { - if (process.env.PLUGIN_WHITELIST && !process.env.PLUGIN_WHITELIST.includes(plugin.name)) + if (!IsPluginAllowed(plugin.name)) { - console.log("Skipping", plugin.name, "missing in whitelist"); - return; - } - - if (process.env.PLUGIN_BLACKLIST && process.env.PLUGIN_BLACKLIST.includes(plugin.name)) - { - console.log("Skipping", plugin.name, "found in whitelist"); + console.log("Skipping", plugin.name, "plugin not allowed"); return; } @@ -101,39 +97,52 @@ export default async function register (pluginManager: PluginManager) await Promise.all(plugins.map(p => registerPlugin(p, 'builtin', pluginManager))); - const storePackageFilePath = path.join(getStoreRootFolder(), 'package.json'); - if (!await Bun.file(storePackageFilePath).exists()) + if (IsPluginAllowed('@simeonradivoev/gameflow-store')) { - console.log("Store is missing. Updating it."); - await taskQueue.enqueue(UpdateStoreJob.id, new UpdateStoreJob()); - console.log("Store Updated"); - } - const storePackage = await Bun.file(storePackageFilePath).json(); - - if (storePackage?.dependencies) - { - const storePlugins = await Promise.all(Object.keys(storePackage.dependencies).filter(p => !blacklist.has(p)).map(async p => + const storePackageFilePath = path.join(getStoreRootFolder(), 'package.json'); + if (!await Bun.file(storePackageFilePath).exists()) { - return getPlugin(p, pluginManager); - })); - - console.log("Checking for outdated packages"); - const outdated = await getUpdates(); - - const validPlugins = storePlugins.filter(p => !!p); - - if (outdated) - { - validPlugins.forEach(p => - { - const newVersion = outdated[p.name]; - if (newVersion) - { - console.log("Plugin", p.name, "has update", p.version, "=>", newVersion); - } - }); + console.log("Store is missing. Updating it."); + await taskQueue.enqueue(EnsureStore.id, new EnsureStore()); + console.log("Store Updated"); } + const storePackage = await Bun.file(storePackageFilePath).json(); - await Promise.all(validPlugins.map(p => registerPlugin(p, 'store', pluginManager))); + if (storePackage?.dependencies) + { + const storePlugins = await Promise.all(Object.keys(storePackage.dependencies).filter(p => !blacklist.has(p)).map(async p => + { + return getPlugin(p, pluginManager); + })); + + console.log("Checking for outdated packages"); + const outdated = await getUpdates(); + + const validPlugins = storePlugins.filter(p => !!p); + + if (outdated) + { + for await (const plugin of validPlugins) + { + const newVersion = outdated[plugin.name]; + if (newVersion) + { + console.log("Plugin", plugin.name, "has update", plugin.version, "=>", newVersion); + } + + if (plugin.autoUpdate) + { + console.log("Auto Updating Plugin", plugin.name); + let response = await runBunPackageCommand(["add", `${plugin.name}@${newVersion}`, "--registry", PluginRegistry, '--omit', 'peer']); + console.log(response); + } + } + } + + await Promise.all(validPlugins.map(p => registerPlugin(p, 'store', pluginManager))); + } + } else + { + console.log('Skipping Store Packages'); } } \ No newline at end of file diff --git a/src/bun/api/plugins/services.ts b/src/bun/api/plugins/services.ts index 9452b7e..0ba40b3 100644 --- a/src/bun/api/plugins/services.ts +++ b/src/bun/api/plugins/services.ts @@ -3,6 +3,7 @@ import os from 'node:os'; import { getStoreRootFolder } from '../store/services/gamesService'; import { PluginDescriptionType } from '@simeonradivoev/gameflow-sdk'; import { run } from 'npm-check-updates'; +import { existsSync } from 'node:fs'; export function canDisable (description: PluginDescriptionType) { @@ -15,6 +16,7 @@ export function canDisable (description: PluginDescriptionType) export async function getUpdates () { + if (!existsSync(getStoreRootFolder())) return {}; const updated = await run({ packageManager: 'bun', peer: true, cwd: getStoreRootFolder(), jsonUpgraded: true, reject: ['@simeonradivoev/gameflow-sdk'] }); return updated as Record; } diff --git a/src/bun/api/store/store.ts b/src/bun/api/store/store.ts index 39d5630..7706699 100644 --- a/src/bun/api/store/store.ts +++ b/src/bun/api/store/store.ts @@ -188,16 +188,16 @@ export const store = new Elysia({ prefix: '/api/store' }) emulator.integrations = integrations; return emulator; }, { params: z.object({ id: z.string() }) }) - .post('/install/emulator/:id/:source', async ({ params: { source, id }, body: { isUpdate } }) => + .post('/install/emulator/:id/:source', async ({ params: { source, id }, body }) => { if (taskQueue.hasActiveOfType(EmulatorDownloadJob)) { return status("Conflict", "Installation already running"); } - const job = new EmulatorDownloadJob(id, source, { isUpdate }); + const job = new EmulatorDownloadJob(id, source, body); return taskQueue.enqueue(EmulatorDownloadJob.id, job); }, { - body: z.object({ isUpdate: z.boolean().optional() }) + body: z.object({ isUpdate: z.boolean().optional() }).optional() }) .delete('/emulator/:id', async ({ params: { id } }) => { diff --git a/src/bun/utils.ts b/src/bun/utils.ts index a3868e4..6fbc630 100644 --- a/src/bun/utils.ts +++ b/src/bun/utils.ts @@ -185,4 +185,19 @@ export function getAppVersion () export function isArchive (path: string) { return archiveRegex.test(path); +} + +export function IsPluginAllowed (id: string) +{ + if (process.env.PLUGIN_WHITELIST && !process.env.PLUGIN_WHITELIST.includes(id)) + { + return false; + } + + if (process.env.PLUGIN_BLACKLIST && process.env.PLUGIN_BLACKLIST.includes(id)) + { + return false; + } + + return true; } \ No newline at end of file diff --git a/src/packages/gameflow-sdk/index.ts b/src/packages/gameflow-sdk/index.ts index 4149891..c78c757 100644 --- a/src/packages/gameflow-sdk/index.ts +++ b/src/packages/gameflow-sdk/index.ts @@ -41,7 +41,8 @@ export const PluginDescriptionSchema = z.object({ peerDependencies: z.record(z.string(), z.string()).optional(), category: z.string().default("other"), main: z.string().describe("The main entry. It must export a default class implementing PluginType"), - canDisable: z.boolean().default(true).optional().describe("Can the plugin be disabled or enabled by the user") + canDisable: z.boolean().default(true).optional().describe("Can the plugin be disabled or enabled by the user"), + autoUpdate: z.boolean().optional().describe("Should the plugin auto update to latest version") }); export const PluginSchema = z.object({ diff --git a/src/packages/gameflow-sdk/task-queue.ts b/src/packages/gameflow-sdk/task-queue.ts index 9ed555e..e86cebc 100644 --- a/src/packages/gameflow-sdk/task-queue.ts +++ b/src/packages/gameflow-sdk/task-queue.ts @@ -91,6 +91,11 @@ export class TaskQueue return this.activeQueue.length > 0; } + public hasQueued () + { + return this.queue && this.queue.length > 0; + } + public hasActiveOfType (type: any) { for (const entry of this.activeQueue) @@ -109,6 +114,30 @@ export class TaskQueue return job?.promise.promise ?? Promise.resolve(); } + public waitForAll () + { + return new Promise((resolve) => + { + if (!this.hasActive()) + { + resolve(true); + return; + } + + const handleEnded = () => + { + if (!this.hasActive() && !this.hasQueued()) + { + resolve(true); + this.events?.removeListener('ended', handleEnded); + this.events?.removeListener('abort', handleEnded); + } + }; + this.events?.on('ended', handleEnded); + this.events?.on('abort', handleEnded); + }); + } + public cancelJob (id: string) { const job = this.queue?.find(j => j.id === id) diff --git a/src/tests/preload.ts b/src/tests/preload.ts index e9a17b9..40cf49d 100644 --- a/src/tests/preload.ts +++ b/src/tests/preload.ts @@ -1,18 +1,20 @@ import { beforeAll, beforeEach, afterEach } from 'bun:test'; import { resolve } from 'node:path'; import * as app from '@/bun/api/app'; -import { remove } from 'fs-extra'; +import { ensureDir, remove } from 'fs-extra'; export async function LoadApp () { console.log("Loading App"); await app.load(); + await app.taskQueue.waitForAll(); } export async function CleanupApp () { console.log("Cleaning Up App"); await app.cleanup(); + await app.resetCleanup(); } beforeAll(async () => @@ -20,7 +22,7 @@ beforeAll(async () => process.env.CUSTOM_STORE_PATH = resolve('./src/tests/mock-store'); process.env.CONFIG_CWD = resolve('./src/tests/mock-config'); process.env.DEFAULT_DOWNLOAD_PATH = resolve('./src/tests/mock-roms'); - process.env.PLUGIN_BLACKLIST = 'com.simeonradivoev.gameflow.rclone'; + process.env.PLUGIN_BLACKLIST = 'com.simeonradivoev.gameflow.rclone,@simeonradivoev/gameflow-store,com.simeonradivoev.gameflow.romm,com.simeonradivoev.gameflow.igdb,@simeonradivoev/gameflow-sdk'; }); async function FileCleanup ()