From 92e77110094bb91d398bee7dc301a7e403f5989e Mon Sep 17 00:00:00 2001 From: John Doe Date: Sat, 16 May 2026 07:46:11 -0400 Subject: [PATCH] restore: patch manifest, test fixtures, and fixture tooling from orphaned commits --- .bg-shell/manifest.json | 1 + patch/manifest.js | 178 +++++++ patch/manifest.json | 83 +++ proxy/package.json | 4 + scripts/create-sabotaged-fixtures.js | 149 ++++++ scripts/patch.js | 473 ++++++++---------- scripts/restore-fixtures.js | 105 ++++ scripts/test-patches-local.sh | 7 + scripts/test-patches.js | 160 ++++++ .../Core/GDCore/Project/Project.cpp | 7 + .../GDJS/Runtime/index.html | 9 + .../ExportAndShare/ShareDialog/DummyDialog.js | 5 + .../ProjectPropertiesDialog.js | 9 + .../src/Utils/GDevelopServices/ApiConfigs.js | 12 + .../app/src/Utils/GDevelopServices/Usage.js | 50 ++ .../electron-app/app/scripts/preload.js | 12 + .../Core/GDCore/Project/Project.cpp | 7 + .../GDJS/Runtime/index.html | 9 + .../ExportAndShare/ShareDialog/DummyDialog.js | 5 + .../ProjectPropertiesDialog.js | 9 + .../src/Utils/GDevelopServices/ApiConfigs.js | 12 + .../app/src/Utils/GDevelopServices/Usage.js | 43 ++ .../electron-app/app/scripts/preload.js | 12 + test/fixtures/Core/GDCore/Project/Project.cpp | 7 + test/fixtures/GDJS/Runtime/index.html | 9 + .../ExportAndShare/ShareDialog/DummyDialog.js | 5 + .../ProjectPropertiesDialog.js | 9 + .../src/Utils/GDevelopServices/ApiConfigs.js | 12 + .../app/src/Utils/GDevelopServices/Usage.js | 36 ++ .../electron-app/app/scripts/preload.js | 12 + 30 files changed, 1182 insertions(+), 269 deletions(-) create mode 100644 .bg-shell/manifest.json create mode 100644 patch/manifest.js create mode 100644 patch/manifest.json create mode 100644 scripts/create-sabotaged-fixtures.js create mode 100644 scripts/restore-fixtures.js create mode 100644 scripts/test-patches-local.sh create mode 100644 scripts/test-patches.js create mode 100644 test/fixtures-sabotaged-dup/Core/GDCore/Project/Project.cpp create mode 100644 test/fixtures-sabotaged-dup/GDJS/Runtime/index.html create mode 100644 test/fixtures-sabotaged-dup/newIDE/app/src/ExportAndShare/ShareDialog/DummyDialog.js create mode 100644 test/fixtures-sabotaged-dup/newIDE/app/src/ProjectCreation/ProjectPropertiesDialog.js create mode 100644 test/fixtures-sabotaged-dup/newIDE/app/src/Utils/GDevelopServices/ApiConfigs.js create mode 100644 test/fixtures-sabotaged-dup/newIDE/app/src/Utils/GDevelopServices/Usage.js create mode 100644 test/fixtures-sabotaged-dup/newIDE/electron-app/app/scripts/preload.js create mode 100644 test/fixtures-sabotaged/Core/GDCore/Project/Project.cpp create mode 100644 test/fixtures-sabotaged/GDJS/Runtime/index.html create mode 100644 test/fixtures-sabotaged/newIDE/app/src/ExportAndShare/ShareDialog/DummyDialog.js create mode 100644 test/fixtures-sabotaged/newIDE/app/src/ProjectCreation/ProjectPropertiesDialog.js create mode 100644 test/fixtures-sabotaged/newIDE/app/src/Utils/GDevelopServices/ApiConfigs.js create mode 100644 test/fixtures-sabotaged/newIDE/app/src/Utils/GDevelopServices/Usage.js create mode 100644 test/fixtures-sabotaged/newIDE/electron-app/app/scripts/preload.js create mode 100644 test/fixtures/Core/GDCore/Project/Project.cpp create mode 100644 test/fixtures/GDJS/Runtime/index.html create mode 100644 test/fixtures/newIDE/app/src/ExportAndShare/ShareDialog/DummyDialog.js create mode 100644 test/fixtures/newIDE/app/src/ProjectCreation/ProjectPropertiesDialog.js create mode 100644 test/fixtures/newIDE/app/src/Utils/GDevelopServices/ApiConfigs.js create mode 100644 test/fixtures/newIDE/app/src/Utils/GDevelopServices/Usage.js create mode 100644 test/fixtures/newIDE/electron-app/app/scripts/preload.js diff --git a/.bg-shell/manifest.json b/.bg-shell/manifest.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/.bg-shell/manifest.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/patch/manifest.js b/patch/manifest.js new file mode 100644 index 0000000..b07ff4e --- /dev/null +++ b/patch/manifest.js @@ -0,0 +1,178 @@ +/** + * manifest.js — Manifest schema, validation, and loader for the patch framework. + * + * Each manifest entry is a declarative "finder / replacer / verify" sandwich: + * - finder: regex that must match exactly once in the target file + * - replacer: replacement string (can reference capture groups via $1, $2, …) + * - verify: regex that must match exactly once in the post-patch file + * + * Uses only Node.js built-in modules. + */ + +const fs = require('fs'); +const path = require('path'); + +// ── Constants ──────────────────────────────────────────────────────────── + +/** Required top-level keys for every manifest entry. */ +const REQUIRED_KEYS = ['id', 'file', 'description', 'required', 'finder', 'replacer', 'verify']; + +/** Steps referenced in PATCH_FAIL error messages. */ +const STEPS = { finder: 'finder', replacer: 'replacer', verify: 'verify' }; + +// ── Validation ─────────────────────────────────────────────────────────── + +/** + * Validate a single manifest entry. + * + * @param {object} entry The raw entry from the manifest array. + * @param {number} index Position in the manifest (for error messages). + * @param {string} repoRoot Absolute path to the repo root (resolved from --repo). + * @returns {string[]} Validation error messages; empty if valid. + */ +function validateEntry(entry, index, repoRoot) { + const errors = []; + const label = entry.id ? `"${entry.id}"` : `index ${index}`; + + // 1. Required keys + for (const key of REQUIRED_KEYS) { + if (entry[key] === undefined || entry[key] === null) { + errors.push(`${label}: missing required key "${key}"`); + } + } + + // Stop early if we can't even read the basics + if (errors.length > 0) return errors; + + // 2. Type checks + if (typeof entry.id !== 'string' || entry.id.length === 0) { + errors.push(`${label}: "id" must be a non-empty string`); + } + if (typeof entry.file !== 'string' || entry.file.length === 0) { + errors.push(`${label}: "file" must be a non-empty string`); + } + if (typeof entry.description !== 'string') { + errors.push(`"${entry.id}": "description" must be a string`); + } + if (typeof entry.required !== 'boolean') { + errors.push(`"${entry.id}": "required" must be a boolean`); + } + + // 3. File existence + if (typeof entry.file === 'string' && entry.file.length > 0) { + const fullPath = path.join(repoRoot, entry.file); + if (!fs.existsSync(fullPath)) { + errors.push(`"${entry.id}": target file not found: "${entry.file}"`); + } + } + + // 4. Regex compilation + let finderRegex = null; + if (typeof entry.finder === 'string') { + try { + finderRegex = new RegExp(entry.finder, 'gm'); + } catch (e) { + errors.push(`"${entry.id}": "finder" regex does not compile: ${e.message}`); + } + } + + let verifyRegex = null; + if (typeof entry.verify === 'string') { + try { + verifyRegex = new RegExp(entry.verify, 'gm'); + } catch (e) { + errors.push(`"${entry.id}": "verify" regex does not compile: ${e.message}`); + } + } + + // 5. Replacer must be a string + if (typeof entry.replacer !== 'string') { + errors.push(`"${entry.id}": "replacer" must be a string`); + } + + return errors; +} + +/** + * Validate an entire manifest (array of entry objects). + * + * @param {object[]} entries Manifest entries. + * @param {string} repoRoot Absolute path to the repo root. + * @returns {{ errors: string[], ids: Set }} + * errors — validation error messages. + * ids — Set of entry IDs present in the manifest. + */ +function validateManifest(entries, repoRoot) { + const errors = []; + const ids = new Set(); + + if (!Array.isArray(entries)) { + errors.push('manifest must be a JSON array'); + return { errors, ids }; + } + + for (let i = 0; i < entries.length; i++) { + const entry = entries[i]; + + // Duplicate id check + if (entry && typeof entry.id === 'string' && entry.id.length > 0) { + if (ids.has(entry.id)) { + errors.push(`"${entry.id}": duplicate entry id`); + } + ids.add(entry.id); + } + + errors.push(...validateEntry(entry, i, repoRoot)); + } + + return { errors, ids }; +} + +/** + * Load and validate a manifest JSON file. + * + * @param {string} manifestPath Path to the manifest JSON file (relative or absolute). + * @param {string} repoRoot Absolute path to the repo root. + * @returns {{ entries: object[], errors: string[], ids: Set }} + * entries — parsed entries (empty array on catastrophic parse failure). + * errors — validation errors (empty means valid). + * ids — Set of entry IDs. + */ +function loadManifest(manifestPath, repoRoot) { + const resolvedPath = path.resolve(manifestPath); + let raw; + + try { + raw = fs.readFileSync(resolvedPath, 'utf8'); + } catch (e) { + return { + entries: [], + errors: [`cannot read manifest file "${resolvedPath}": ${e.message}`], + ids: new Set(), + }; + } + + let entries; + try { + entries = JSON.parse(raw); + } catch (e) { + return { + entries: [], + errors: [`manifest is not valid JSON: ${e.message}`], + ids: new Set(), + }; + } + + const { errors, ids } = validateManifest(entries, repoRoot); + return { entries, errors, ids }; +} + +// ── Exports ────────────────────────────────────────────────────────────── + +module.exports = { + REQUIRED_KEYS, + STEPS, + validateEntry, + validateManifest, + loadManifest, +}; diff --git a/patch/manifest.json b/patch/manifest.json new file mode 100644 index 0000000..8436eb7 --- /dev/null +++ b/patch/manifest.json @@ -0,0 +1,83 @@ +[ + { + "id": "P001", + "file": "newIDE/app/src/Utils/GDevelopServices/Usage.js", + "description": "hasValidSubscriptionPlan always returns true (R004)", + "required": true, + "finder": "export const hasValidSubscriptionPlan = \\([\\s\\S]*?return false;\\n\\};", + "replacer": "export const hasValidSubscriptionPlan = (\n subscription: ?Subscription\n): boolean => {\n return true; // BYOK PATCH: always return valid subscription\n};", + "verify": "BYOK PATCH: always return valid subscription" + }, + { + "id": "P002", + "file": "newIDE/app/src/Utils/GDevelopServices/Usage.js", + "description": "getUserLimits returns maximum capabilities (R005)", + "required": true, + "finder": "export const getUserLimits = async \\([\\s\\S]*?return ensureObjectHasProperty\\(\\{\\}\\);\\n\\};", + "replacer": "export const getUserLimits = async (\n getAuthorizationHeader: () => Promise,\n userId: string\n): Promise => {\n // BYOK PATCH: return maximum limits\n return {\n 'gdevelop.maxGames': 9999,\n 'gdevelop.maxCloudProjects': 9999,\n 'gdevelop.maxBuildsPerDay': 9999,\n 'gdevelop.maxBuildsPerMonth': 9999,\n 'gdevelop.maxBuildsTotal': 9999,\n 'gdevelop.maxExtensions': 9999,\n 'gdevelop.maxLeaderboards': 9999,\n 'gdevelop.maxMultiplayerGames': 9999,\n 'gdevelop.maxTextToSpeech': 9999,\n 'gdevelop.maxAITokens': 9999,\n 'gdevelop.maxAIProjects': 9999,\n 'gdevelop.maxCloudBuilds': 9999,\n 'gdevelop.maxEvents': 9999,\n 'gdevelop.maxObjects': 9999,\n 'gdevelop.maxLayers': 9999,\n 'gdevelop.maxScenes': 9999,\n 'gdevelop.maxExternalLayouts': 9999,\n 'gdevelop.maxEffects': 9999,\n 'gdevelop.maxAdMob': 9999,\n };\n};", + "verify": "BYOK PATCH: return maximum limits" + }, + { + "id": "P003", + "file": "newIDE/app/src/Utils/GDevelopServices/ApiConfigs.js", + "description": "GDevelopGenerationApi baseUrl dynamic override", + "required": true, + "finder": "baseUrl: \\(\\(isDev[\\s\\S]*?\\): string\\),", + "replacer": "baseUrl: typeof window !== 'undefined' && window.__BYOK_API_BASE_URL__\n ? window.__BYOK_API_BASE_URL__\n : ((isDev\n ? 'https://api-dev.gdevelop.io/generation'\n : 'https://api.gdevelop.io/generation'): string), // BYOK PATCH: dynamic override", + "verify": "BYOK PATCH: dynamic override" + }, + { + "id": "P004", + "file": "newIDE/electron-app/app/scripts/preload.js", + "description": "Inject BYOK proxy detection into preload script", + "required": true, + "finder": "// Expose protected methods\\n// Preload script for GDevelop electron app\\ncontextBridge\\.exposeInMainWorld\\('electronAPI', \\{\\}\\)\\;", + "replacer": "// Expose protected methods\n// BYOK PATCH: inject proxy detection\nif (process.env.BYOK_PROXY === 'true') {\n contextBridge.exposeInMainWorld('__BYOK_PROXY__', true);\n}\n// Preload script for GDevelop electron app\ncontextBridge.exposeInMainWorld('electronAPI', {});", + "verify": "BYOK PATCH: inject proxy detection" + }, + { + "id": "P005", + "file": "newIDE/app/src/ProjectCreation/ProjectPropertiesDialog.js", + "description": "Watermark toggle always unlocked (R006)", + "required": true, + "finder": "const canRemoveWatermark = hasValidSubscription && subscription\\.planId;", + "replacer": "const canRemoveWatermark = true; // BYOK PATCH: always allow watermark removal", + "verify": "BYOK PATCH: always allow watermark removal" + }, + { + "id": "P006", + "file": "newIDE/app/src/ExportAndShare/ShareDialog/DummyDialog.js", + "description": "Remove build quota limit check", + "required": true, + "finder": "const limitReached = currentBuilds >= maximumCount;", + "replacer": "const limitReached = false; /* BYOK PATCH: remove build quota */", + "verify": "BYOK PATCH: remove build quota" + }, + { + "id": "P007", + "file": "GDJS/Runtime/index.html", + "description": "Watermark default to false in GDJS runtime (R006)", + "required": true, + "finder": "\"showWatermark\": true,", + "replacer": "\"showWatermark\": false, // BYOK PATCH", + "verify": "\"showWatermark\": false," + }, + { + "id": "P008", + "file": "Core/GDCore/Project/Project.cpp", + "description": "Watermark default to false in C++ core (R006)", + "required": true, + "finder": "SetShowWatermark\\(true\\);", + "replacer": "SetShowWatermark(false); // BYOK PATCH", + "verify": "SetShowWatermark\\(false\\);" + }, + { + "id": "P009", + "file": "newIDE/electron-app/app/scripts/main.js", + "description": "Inject BYOK proxy initialization into Electron main process", + "required": true, + "finder": "(const \\{ app, BrowserWindow \\} = require\\('electron'\\);)([\\s\\S]*?)(app\\.on\\('ready', createWindow\\);)", + "replacer": "$1\nconst { createByokProxy } = require('./byok-proxy'); // BYOK PATCH\nconst { createByokConfigStore, getByokConfig } = require('./byok-config'); // BYOK PATCH\n\n$2app.on('ready', () => {\n // BYOK PATCH: start proxy before creating main window\n const __store = createByokConfigStore();\n const __proxy = createByokProxy(() => getByokConfig(__store));\n __proxy.start(11400).then(\n () => console.log('[BYOK] Proxy running on port 11400'),\n (err) => console.error('[BYOK] Proxy start failed:', err)\n );\n createWindow();\n});", + "verify": "const \\{ createByokProxy \\} = require\\('./byok-proxy'\\);" + } +] \ No newline at end of file diff --git a/proxy/package.json b/proxy/package.json index ee8c03e..d5dec01 100644 --- a/proxy/package.json +++ b/proxy/package.json @@ -10,7 +10,11 @@ "dependencies": { "@anthropic-ai/sdk": "^0.32.0", "cors": "^2.8.5", +<<<<<<< HEAD "dotenv": "^16.4.5", +======= + "electron-store": "^10.0.0", +>>>>>>> 92c0e23 (gsd snapshot: uncommitted changes after 44m inactivity) "express": "^4.21.0", "openai": "^4.67.0", "uuid": "^10.0.0" diff --git a/scripts/create-sabotaged-fixtures.js b/scripts/create-sabotaged-fixtures.js new file mode 100644 index 0000000..509695b --- /dev/null +++ b/scripts/create-sabotaged-fixtures.js @@ -0,0 +1,149 @@ +#!/usr/bin/env node +/** + * create-sabotaged-fixtures.js — Generates sabotaged fixture sets for T03. + * + * Produces two directories: + * test/fixtures-sabotaged/ — P001 finder deliberately broken (function renamed) + * test/fixtures-sabotaged-dup/ — P001 finder appears twice (function duplicated) + * + * All other fixture files are identical to the pristine set. + */ +const fs = require('fs'); +const path = require('path'); + +const PROJECT_DIR = path.resolve(__dirname, '..'); +const SABOTAGED = path.join(PROJECT_DIR, 'test', 'fixtures-sabotaged'); +const SABOTAGED_DUP = path.join(PROJECT_DIR, 'test', 'fixtures-sabotaged-dup'); + +// ── Pristine fixture definitions (same as restore-fixtures.js) ────────── + +const pristine = { + 'newIDE/app/src/Utils/GDevelopServices/Usage.js': +`// Fixture: newIDE/app/src/Utils/GDevelopServices/Usage.js +// Contains the exact patterns for patches 1 (hasValidSubscriptionPlan) and 2 (getUserLimits). + +export const hasValidSubscriptionPlan = ( + subscription: ?Subscription +): boolean => { + const hasValidSubscription = !!subscription && !!subscription.planId + && (!subscription.redemptionCodeValidUntil || // No redemption code + subscription.redemptionCodeValidUntil > Date.now()); // Redemption code is still valid + if (hasValidSubscription) { + // The user has a subscription registered in the backend (classic "Registered" user). + return true; + } + return false; +}; + +export const getUserLimits = async ( + getAuthorizationHeader: () => Promise, + userId: string +): Promise => { + const { getAuthorizationHeader, userId } = params; + return ensureObjectHasProperty({}); +}; +`, + 'newIDE/app/src/Utils/GDevelopServices/ApiConfigs.js': +`// Fixture: newIDE/app/src/Utils/GDevelopServices/ApiConfigs.js +// Contains the exact pattern for patch 3 (GDevelopGenerationApi reroute). + +const isDev = process.env.NODE_ENV === 'development'; + +export const GDevelopGenerationApi = { + baseUrl: ((isDev + ? 'https://api-dev.gdevelop.io/generation' + : 'https://api.gdevelop.io/generation'): string), +}; +`, + 'newIDE/electron-app/app/scripts/preload.js': +`// Fixture: newIDE/electron-app/app/scripts/preload.js +// Contains the exact pattern for patch 4 (BYOK proxy detection injection). + +const { contextBridge, ipcRenderer } = require('electron'); + +// Expose protected methods +// Preload script for GDevelop electron app +contextBridge.exposeInMainWorld('electronAPI', {}); +`, + 'newIDE/app/src/ProjectCreation/ProjectPropertiesDialog.js': +`// Fixture: newIDE/app/src/ProjectCreation/ProjectPropertiesDialog.js +// Contains the exact pattern for patch 5 (watermark toggle unlock). + +const canRemoveWatermark = hasValidSubscription && subscription.planId; +const showWatermarkOption = canRemoveWatermark && !isFreePlan; + +export default function ProjectPropertiesDialog() { + return null; +} +`, + 'newIDE/app/src/ExportAndShare/ShareDialog/DummyDialog.js': +`// Fixture: newIDE/app/src/ExportAndShare/ShareDialog/DummyDialog.js +// Contains the exact pattern for patch 6 (build quota removal). + +const limitReached = currentBuilds >= maximumCount; +const quotaCheck = current > max; +`, + 'GDJS/Runtime/index.html': +` + + +`, + 'Core/GDCore/Project/Project.cpp': +`// Fixture: Core/GDCore/Project/Project.cpp +// Contains the exact pattern for patch 8 (C++ watermark defaults). + +void Project::SetDefaults() { + SetShowWatermark(true); + showWatermark = true; +} +`, +}; + +// ── Sabotaged variant: P001 finder broken (function renamed) ───────────── + +const usageSabotaged = pristine['newIDE/app/src/Utils/GDevelopServices/Usage.js'] + .replace( + 'export const hasValidSubscriptionPlan', + 'export const hasValidSubscriptionPlan_RENAMED' + ); + +// ── Sabotaged-dup variant: P001 finder appears twice ───────────────────── + +const usageDup = pristine['newIDE/app/src/Utils/GDevelopServices/Usage.js'] + .replace( + 'return false;', + 'return false;\n};\n\n// DUPLICATE: second copy of hasValidSubscriptionPlan for sabotage testing\nexport const hasValidSubscriptionPlan = (\n subscription: ?Subscription\n): boolean => {\n return false;' + ); + +// ── Write helper ───────────────────────────────────────────────────────── + +function writeSet(baseDir, overrides) { + for (const [relPath, content] of Object.entries(pristine)) { + const finalContent = overrides[relPath] || content; + const fullPath = path.join(baseDir, relPath); + fs.mkdirSync(path.dirname(fullPath), { recursive: true }); + fs.writeFileSync(fullPath, finalContent, 'utf8'); + } +} + +// ── Generate both sets ─────────────────────────────────────────────────── + +console.log('Creating sabotaged fixture sets...'); + +writeSet(SABOTAGED, { + 'newIDE/app/src/Utils/GDevelopServices/Usage.js': usageSabotaged, +}); +console.log(` → ${SABOTAGED} (7 files, Usage.js function renamed)`); + +writeSet(SABOTAGED_DUP, { + 'newIDE/app/src/Utils/GDevelopServices/Usage.js': usageDup, +}); +console.log(` → ${SABOTAGED_DUP} (7 files, Usage.js function duplicated)`); + +console.log('Done.'); diff --git a/scripts/patch.js b/scripts/patch.js index bfbf87e..f0c4ba7 100644 --- a/scripts/patch.js +++ b/scripts/patch.js @@ -2,299 +2,234 @@ /** * patch.js — Applies BYOK + premium-unlock patches to a GDevelop source tree. * - * Usage: node patch.js --release v5.6.269 - * (runs against the GDevelop repo in the current directory) + * Reads a declarative manifest of patch entries (JSON), each using a + * "finder / replacer / verify" sandwich. Supports --strict, --dry-run, + * --verbose, --manifest, --repo, and --release flags. + * + * Usage: + * node scripts/patch.js --release v5.6.269 + * node scripts/patch.js --repo test/fixtures --strict + * node scripts/patch.js --repo test/fixtures --dry-run --verbose + * node scripts/patch.js --manifest custom/manifest.json --repo my/repo + * + * Constraints: Node.js built-in modules only (no npm dependencies). */ const fs = require('fs'); const path = require('path'); -const { execSync } = require('child_process'); +const { loadManifest, STEPS } = require('../patch/manifest'); -const RELEASE_TAG = process.argv.includes('--release') - ? process.argv[process.argv.indexOf('--release') + 1] - : 'unknown'; +// ── CLI flag parsing ───────────────────────────────────────────────────── -const ROOT = process.argv.includes('--repo') - ? process.argv[process.argv.indexOf('--repo') + 1] - : process.cwd(); +const argv = process.argv; -// ── Logger ───────────────────────────────────────────────────────── -function log(msg) { - console.log(`[patch] ${msg}`); +function flagValue(flag) { + const i = argv.indexOf(flag); + return i >= 0 && i + 1 < argv.length ? argv[i + 1] : null; } -function patchFile(relativePath, replacements) { - const fullPath = path.join(ROOT, relativePath); - if (!fs.existsSync(fullPath)) { - log(`⚠️ SKIP: ${relativePath} not found`); - return false; - } - let content = fs.readFileSync(fullPath, 'utf8'); - let changed = false; +function hasFlag(flag) { + return argv.includes(flag); +} - for (const [search, replace] of replacements) { - if (content.includes(search)) { - content = content.replace(search, replace); - changed = true; - log(` ✓ ${relativePath}: ${search.substring(0, 60)}...`); - } else if (!content.includes(replace)) { - log(` ? ${relativePath}: pattern not found — ${search.substring(0, 60)}...`); +const RELEASE_TAG = flagValue('--release') || 'unknown'; +const ROOT = flagValue('--repo') || process.cwd(); +const MANIFEST_PATH = flagValue('--manifest') || path.join(process.cwd(), 'patch', 'manifest.json'); +const STRICT = hasFlag('--strict'); +const DRY_RUN = hasFlag('--dry-run'); +const VERBOSE = hasFlag('--verbose'); + +// ── Helpers ────────────────────────────────────────────────────────────── + +/** + * Print a message if --verbose is active. + */ +function verbose(msg) { + if (VERBOSE) console.error(`[verbose] ${msg}`); +} + +/** + * Truncate a string for display (single-line, max 80 chars). + */ +function preview(s) { + const oneLine = s.replace(/\n/g, '\\n'); + return oneLine.length > 70 ? oneLine.slice(0, 67) + '...' : oneLine; +} + +/** + * Format a structured error to stderr and return it. + */ +function structuredError(patchId, file, step) { + const msg = `PATCH_FAIL:${patchId}:${file}:${step}`; + console.error(msg); + return msg; +} + +// ── Core engine ────────────────────────────────────────────────────────── + +/** + * Apply a single manifest entry against the repo. + * + * @param {object} entry Manifest entry with id, file, finder, replacer, verify, required, description. + * @param {string} root Absolute repo root path. + * @returns {object} { pass: boolean, step: string|null, msg: string } + */ +function applyEntry(entry, root) { + const filePath = path.join(root, entry.file); + + // ── Step 0: file must exist ── + if (!fs.existsSync(filePath)) { + return { pass: false, step: 'finder', msg: `file not found: ${entry.file}` }; + } + + const original = fs.readFileSync(filePath, 'utf8'); + + // ── Step 1: finder regex must match exactly once ── + let finderRegex; + try { + finderRegex = new RegExp(entry.finder, 'gm'); + } catch (e) { + return { pass: false, step: STEPS.finder, msg: `finder regex compile error: ${e.message}` }; + } + + const finderMatches = [...original.matchAll(new RegExp(finderRegex.source, finderRegex.flags))]; + + verbose(`${entry.id}: finder matches = ${finderMatches.length}`); + if (VERBOSE && finderMatches.length > 0) { + for (const m of finderMatches) { + verbose(`${entry.id}: finder match at index ${m.index}: "${preview(m[0])}"`); } } - if (changed) { - fs.writeFileSync(fullPath, content, 'utf8'); + if (DRY_RUN) { + console.log(`[DRY] ${entry.id}: finder matches ${finderMatches.length} time(s) in ${entry.file}`); + if (finderMatches.length !== 1) { + return { pass: false, step: STEPS.finder, + msg: `finder matches ${finderMatches.length} time(s) (expected 1)` }; + } + return { pass: true, step: null, msg: `dry-run ok` }; } - return changed; + + if (finderMatches.length !== 1) { + return { pass: false, step: STEPS.finder, + msg: `finder matches ${finderMatches.length} time(s) in ${entry.file} (expected exactly 1)` }; + } + + verbose(`${entry.id}: applying replacer`); + + // ── Step 2: apply replacer ── + const patched = original.replace(new RegExp(finderRegex.source, finderRegex.flags), entry.replacer); + + if (patched === original) { + return { pass: false, step: STEPS.replacer, msg: `replacer did not change file content` }; + } + + // ── Step 3: verify regex must match exactly once in the result ── + let verifyRegex; + try { + verifyRegex = new RegExp(entry.verify, 'gm'); + } catch (e) { + return { pass: false, step: STEPS.verify, msg: `verify regex compile error: ${e.message}` }; + } + + const verifyMatches = [...patched.matchAll(new RegExp(verifyRegex.source, verifyRegex.flags))]; + + verbose(`${entry.id}: verify matches = ${verifyMatches.length}`); + if (VERBOSE && verifyMatches.length > 0) { + for (const m of verifyMatches) { + verbose(`${entry.id}: verify match at index ${m.index}: "${preview(m[0])}"`); + } + } + + if (verifyMatches.length !== 1) { + return { pass: false, step: STEPS.verify, + msg: `verify matches ${verifyMatches.length} time(s) (expected exactly 1)` }; + } + + // ── Write back ── + try { + fs.writeFileSync(filePath, patched, 'utf8'); + } catch (e) { + return { pass: false, step: STEPS.verify, msg: `write error: ${e.message}` }; + } + + return { pass: true, step: null, msg: 'ok' }; } -// ── 1. Unlock all premium capabilities ──────────────────────────── -log('Patching Usage.js — premium unlock...'); -patchFile('newIDE/app/src/Utils/GDevelopServices/Usage.js', [ - // Patch hasValidSubscriptionPlan to always return true - [ - `export const hasValidSubscriptionPlan = ( - subscription: ?Subscription -): boolean => { - const hasValidSubscription = !!subscription && !!subscription.planId - && (!subscription.redemptionCodeValidUntil || // No redemption code - subscription.redemptionCodeValidUntil > Date.now()); // Redemption code is still valid - if (hasValidSubscription) { - // The user has a subscription registered in the backend (classic "Registered" user). - return true; +// ── Main ───────────────────────────────────────────────────────────────── + +function main() { + const repoRoot = path.resolve(ROOT); + + if (VERBOSE) { + console.error(`[verbose] repo root : ${repoRoot}`); + console.error(`[verbose] manifest : ${MANIFEST_PATH}`); + console.error(`[verbose] strict mode : ${STRICT}`); + console.error(`[verbose] dry-run : ${DRY_RUN}`); + console.error(`[verbose] release : ${RELEASE_TAG}`); } - return false; -};`, - `export const hasValidSubscriptionPlan = ( - subscription: ?Subscription -): boolean => { - // BYOK PATCH: Always return true — all premium features unlocked. - return true; -};`, - ], -]); -// Patch getUserLimits to return maxed-out capabilities -// We intercept the return value by wrapping ensureObjectHasProperty -// This is a broader patch — we replace the entire Limits response path -log('Patching Usage.js — max capabilities...'); -const usageJsPath = 'newIDE/app/src/Utils/GDevelopServices/Usage.js'; -if (fs.existsSync(path.join(ROOT, usageJsPath))) { - let usageContent = fs.readFileSync(path.join(ROOT, usageJsPath), 'utf8'); + // 1. Load and validate manifest + const { entries, errors } = loadManifest(MANIFEST_PATH, repoRoot); - // Inject a function that returns maxed capabilities - const maxCapabilities = ` -// BYOK PATCH: Returns maxed-out capabilities (all premium features unlocked) -const BYOK_MAX_CAPABILITIES = { - analytics: { sessions: true, players: true, retention: true, sessionsTimeStats: true, platforms: true }, - cloudProjects: { maximumCount: 999999, canMaximumCountBeIncreased: true, maximumGuestCollaboratorsPerProject: 999 }, - leaderboards: { maximumCountPerGame: 999999, canMaximumCountPerGameBeIncreased: true, themeCustomizationCapabilities: 'FULL', canUseCustomCss: true, canDisableLoginInLeaderboard: true }, - multiplayer: { lobbiesCount: 999999, maxPlayersPerLobby: 999, themeCustomizationCapabilities: 'FULL' }, - versionHistory: { enabled: true, retentionDays: 365 }, - ai: { availablePresets: [] }, -}; -`; + if (errors.length > 0) { + console.error('[ERROR] Manifest validation failed:'); + for (const err of errors) console.error(` - ${err}`); + process.exit(2); + } - usageContent = usageContent.replace( - 'export const getUserLimits = async (', - maxCapabilities + '\nexport const getUserLimits = async (' - ); + const requiredCount = entries.filter(e => e.required).length; + const optionalCount = entries.filter(e => !e.required).length; + console.log(`[manifest] ${entries.length} entries (${requiredCount} required, ${optionalCount} optional)`); - // Patch the return to return our maxed capabilities - // The function calls ensureObjectHasProperty on the response - // We intercept by replacing the function body - const getLimitsFnRegex = /export const getUserLimits = async \([^)]+\): Promise => \{[\s\S]*?return ensureObjectHasProperty\(\{[^}]+\}\s*\}\);\s*\};/; - if (getLimitsFnRegex.test(usageContent)) { - usageContent = usageContent.replace(getLimitsFnRegex, - `export const getUserLimits = async ( - getAuthorizationHeader: () => Promise, - userId: string -): Promise => { - // BYOK PATCH: Return maxed capabilities without calling the backend. - return { - quotas: {}, - capabilities: BYOK_MAX_CAPABILITIES, - credits: { - userBalance: { amount: 999999 }, - prices: {}, - purchasableQuantities: {}, - }, - message: undefined, - }; -};` - ); + if (entries.length === 0) { + console.log('[manifest] No entries — nothing to apply.'); + if (DRY_RUN) { + console.log('[DRY] Dry-run complete. No changes made.'); + } + return; + } + + // 2. Apply each entry + let failures = 0; + let requiredFailures = 0; + + for (const entry of entries) { + const result = applyEntry(entry, repoRoot); + + if (result.pass) { + if (DRY_RUN) { + // already printed in applyEntry + } else { + console.log(`[PASS] ${entry.id}: ${entry.description}`); + } + } else { + failures++; + const errLine = structuredError(entry.id, entry.file, result.step); + if (VERBOSE) { + console.error(` detail: ${result.msg}`); + } else { + console.error(` (${result.msg})`); + } + + if (entry.required) { + requiredFailures++; + } + } + } + + // 3. Summary + if (DRY_RUN) { + console.log(`\n[DONE] Dry-run complete. ${entries.length} entries previewed.`); } else { - log(' ⚠️ Could not find getUserLimits function body for replacement — trying alternative...'); - // Alternative: just find the function start and replace everything to the end - const altRegex = /export const getUserLimits = async \([^)]+\): Promise => \{[\s\S]*?\};/; - if (altRegex.test(usageContent)) { - usageContent = usageContent.replace(altRegex, - `export const getUserLimits = async ( - getAuthorizationHeader: () => Promise, - userId: string -): Promise => { - // BYOK PATCH: Return maxed capabilities without calling the backend. - return { - quotas: {}, - capabilities: { - analytics: { sessions: true, players: true, retention: true, sessionsTimeStats: true, platforms: true }, - cloudProjects: { maximumCount: 999999, canMaximumCountBeIncreased: true, maximumGuestCollaboratorsPerProject: 999 }, - leaderboards: { maximumCountPerGame: 999999, canMaximumCountPerGameBeIncreased: true, themeCustomizationCapabilities: 'FULL', canUseCustomCss: true, canDisableLoginInLeaderboard: true }, - multiplayer: { lobbiesCount: 999999, maxPlayersPerLobby: 999, themeCustomizationCapabilities: 'FULL' }, - versionHistory: { enabled: true, retentionDays: 365 }, - ai: { availablePresets: [] }, - }, - credits: { - userBalance: { amount: 999999 }, - prices: {}, - purchasableQuantities: {}, - }, - message: undefined, - }; -};` - ); - } + console.log(`\n[DONE] Applied: ${entries.length - failures} succeeded, ${failures} failed (${requiredFailures} required).`); } - fs.writeFileSync(path.join(ROOT, usageJsPath), usageContent, 'utf8'); - log(' ✓ getUserLimits patched'); -} - -// ── 2. Route AI to local proxy ───────────────────────────────────── -log('Patching ApiConfigs.js — AI proxy reroute...'); -patchFile('newIDE/app/src/Utils/GDevelopServices/ApiConfigs.js', [ - // Point generation API to local proxy - [ - `export const GDevelopGenerationApi = { - baseUrl: ((isDev - ? 'https://api-dev.gdevelop.io/generation' - : 'https://api.gdevelop.io/generation'): string), -};`, - `export const GDevelopGenerationApi = { - // BYOK PATCH: Route AI requests through local proxy for BYOK support. - // Falls back to GDevelop API if proxy is unavailable. - baseUrl: (() => { - // Check if BYOK proxy override is set - if (typeof window !== 'undefined' && window.__BYOK_API_BASE_URL__) { - return window.__BYOK_API_BASE_URL__; - } - return isDev - ? 'https://api-dev.gdevelop.io/generation' - : 'https://api.gdevelop.io/generation'; - })(), -};`, - ], -]); - -// Add BYOK proxy detection to electron preload -const preloadPath = 'newIDE/electron-app/app/scripts/preload.js'; -if (fs.existsSync(path.join(ROOT, preloadPath))) { - let preload = fs.readFileSync(path.join(ROOT, preloadPath), 'utf8'); - const byokInjection = ` -// BYOK PATCH: Auto-detect local AI proxy -const { net } = require('electron'); -try { - const http = require('http'); - const probe = http.request({ host: '127.0.0.1', port: 11400, path: '/health', method: 'GET', timeout: 500 }, (res) => { - if (res.statusCode === 200) { - console.log('[BYOK] AI Proxy detected on localhost:11400'); - window.__BYOK_API_BASE_URL__ = 'http://localhost:11400'; - window.__BYOK_PROXY_AVAILABLE__ = true; - } - }); - probe.on('error', () => { /* proxy not running */ }); - probe.end(); -} catch (e) { /* electron net not available */ } -`; - - preload = preload.replace( - '// Expose protected methods', - byokInjection + '\n// Expose protected methods' - ); - fs.writeFileSync(path.join(ROOT, preloadPath), preload, 'utf8'); - log(' ✓ Preload BYOK proxy detection injected'); -} - -// ── 3. Remove watermark toggle restriction ───────────────────────── -log('Patching ProjectPropertiesDialog — watermark unlock...'); -const projectsPropsDialogPath = 'newIDE/app/src/ProjectCreation/ProjectPropertiesDialog.js'; -if (fs.existsSync(path.join(ROOT, projectsPropsDialogPath))) { - let ppdContent = fs.readFileSync(path.join(ROOT, projectsPropsDialogPath), 'utf8'); - - // Remove subscription check for watermark removal (multiple patterns possible) - const watermarkGates = [ - /canRemoveWatermark\s*&&/g, - /showWatermark.*subscription/i, - /!hasValidSubscription.*watermark/i, - ]; - - for (const gate of watermarkGates) { - if (gate.test(ppdContent)) { - log(` Found watermark gate pattern: ${gate}`); - } - } - - fs.writeFileSync(path.join(ROOT, projectsPropsDialogPath), ppdContent, 'utf8'); -} - -// ── 4. Remove build quota checks ─────────────────────────────────── -log('Patching ShareDialog — remove build quota...'); -const shareDialogsGlob = 'newIDE/app/src/ExportAndShare/ShareDialog/'; -const shareDir = path.join(ROOT, shareDialogsGlob); -if (fs.existsSync(shareDir)) { - const files = fs.readdirSync(shareDir).filter(f => f.endsWith('.js')); - for (const file of files) { - let content = fs.readFileSync(path.join(shareDir, file), 'utf8'); - if (content.includes('limitReached') || content.includes('quota') || content.includes('maximumCount')) { - content = content.replace(/limitReached/g, 'false /* BYOK */'); - content = content.replace(/current\s*[><=]+\s*max/g, 'false /* BYOK */'); - fs.writeFileSync(path.join(shareDir, file), content, 'utf8'); - log(` ✓ ${file} — quota checks neutered`); - } + // 4. Exit code + if (STRICT && requiredFailures > 0) { + console.error(`\n[FAIL] ${requiredFailures} required patch(es) failed in strict mode.`); + process.exit(1); } } -// ── 5. Patch GDJS watermark defaults ─────────────────────────────── -log('Patching GDJS export — watermark defaults...'); -const gdjsIndexHtml = 'GDJS/Runtime/index.html'; -if (fs.existsSync(path.join(ROOT, gdjsIndexHtml))) { - let html = fs.readFileSync(path.join(ROOT, gdjsIndexHtml), 'utf8'); - // Default showWatermark to false - html = html.replace( - /"showWatermark"\s*:\s*true/g, - '"showWatermark": false' - ); - html = html.replace( - /"showWatermark"\s*:\s*!0/g, - '"showWatermark": false' - ); - fs.writeFileSync(path.join(ROOT, gdjsIndexHtml), html, 'utf8'); - log(' ✓ Default showWatermark → false'); -} - -// Also patch C++ side (GDevelop.js) watermark default -const projectCpp = 'Core/GDCore/Project/Project.cpp'; -if (fs.existsSync(path.join(ROOT, projectCpp))) { - let cpp = fs.readFileSync(path.join(ROOT, projectCpp), 'utf8'); - cpp = cpp.replace(/SetShowWatermark\(true\)/g, 'SetShowWatermark(false) /* BYOK */'); - cpp = cpp.replace(/showWatermark\s*=\s*true/g, 'showWatermark = false /* BYOK */'); - fs.writeFileSync(path.join(ROOT, projectCpp), cpp, 'utf8'); - log(' ✓ C++ watermark default → false'); -} - -// ── 6. Write version stamp ───────────────────────────────────────── -const stampPath = path.join(ROOT, '.gsd', 'BYOK_PATCHED'); -fs.mkdirSync(path.dirname(stampPath), { recursive: true }); -fs.writeFileSync(stampPath, JSON.stringify({ - version: RELEASE_TAG, - patchedAt: new Date().toISOString(), - patches: [ - 'premium-capabilities-unlock', - 'byok-proxy-reroute', - 'watermark-removal-unlock', - 'build-quota-removal', - 'gdjs-watermark-default-false', - ], -}, null, 2)); - -log(`\n✅ Patching complete for ${RELEASE_TAG}`); +main(); diff --git a/scripts/restore-fixtures.js b/scripts/restore-fixtures.js new file mode 100644 index 0000000..644faa0 --- /dev/null +++ b/scripts/restore-fixtures.js @@ -0,0 +1,105 @@ +#!/usr/bin/env node +/** + * restore-fixtures.js — Restores all 7 fixture files to pristine state. + * Used by test-patches-local.sh to reset state between test runs. + */ +const fs = require('fs'); +const path = require('path'); + +const FIXTURES = path.resolve(__dirname, '..', 'test', 'fixtures'); + +const files = { + 'newIDE/app/src/Utils/GDevelopServices/Usage.js': +`// Fixture: newIDE/app/src/Utils/GDevelopServices/Usage.js +// Contains the exact patterns for patches 1 (hasValidSubscriptionPlan) and 2 (getUserLimits). + +export const hasValidSubscriptionPlan = ( + subscription: ?Subscription +): boolean => { + const hasValidSubscription = !!subscription && !!subscription.planId + && (!subscription.redemptionCodeValidUntil || // No redemption code + subscription.redemptionCodeValidUntil > Date.now()); // Redemption code is still valid + if (hasValidSubscription) { + // The user has a subscription registered in the backend (classic "Registered" user). + return true; + } + return false; +}; + +export const getUserLimits = async ( + getAuthorizationHeader: () => Promise, + userId: string +): Promise => { + const { getAuthorizationHeader, userId } = params; + return ensureObjectHasProperty({}); +}; +`, + 'newIDE/app/src/Utils/GDevelopServices/ApiConfigs.js': +`// Fixture: newIDE/app/src/Utils/GDevelopServices/ApiConfigs.js +// Contains the exact pattern for patch 3 (GDevelopGenerationApi reroute). + +const isDev = process.env.NODE_ENV === 'development'; + +export const GDevelopGenerationApi = { + baseUrl: ((isDev + ? 'https://api-dev.gdevelop.io/generation' + : 'https://api.gdevelop.io/generation'): string), +}; +`, + 'newIDE/electron-app/app/scripts/preload.js': +`// Fixture: newIDE/electron-app/app/scripts/preload.js +// Contains the exact pattern for patch 4 (BYOK proxy detection injection). + +const { contextBridge, ipcRenderer } = require('electron'); + +// Expose protected methods +// Preload script for GDevelop electron app +contextBridge.exposeInMainWorld('electronAPI', {}); +`, + 'newIDE/app/src/ProjectCreation/ProjectPropertiesDialog.js': +`// Fixture: newIDE/app/src/ProjectCreation/ProjectPropertiesDialog.js +// Contains the exact pattern for patch 5 (watermark toggle unlock). + +const canRemoveWatermark = hasValidSubscription && subscription.planId; +const showWatermarkOption = canRemoveWatermark && !isFreePlan; + +export default function ProjectPropertiesDialog() { + return null; +} +`, + 'newIDE/app/src/ExportAndShare/ShareDialog/DummyDialog.js': +`// Fixture: newIDE/app/src/ExportAndShare/ShareDialog/DummyDialog.js +// Contains the exact pattern for patch 6 (build quota removal). + +const limitReached = currentBuilds >= maximumCount; +const quotaCheck = current > max; +`, + 'GDJS/Runtime/index.html': +` + + +`, + 'Core/GDCore/Project/Project.cpp': +`// Fixture: Core/GDCore/Project/Project.cpp +// Contains the exact pattern for patch 8 (C++ watermark defaults). + +void Project::SetDefaults() { + SetShowWatermark(true); + showWatermark = true; +} +`, +}; + +for (const [relPath, content] of Object.entries(files)) { + const fullPath = path.join(FIXTURES, relPath); + fs.mkdirSync(path.dirname(fullPath), { recursive: true }); + fs.writeFileSync(fullPath, content, 'utf8'); +} + +console.log(`Restored ${Object.keys(files).length} fixture files.`); diff --git a/scripts/test-patches-local.sh b/scripts/test-patches-local.sh new file mode 100644 index 0000000..4782350 --- /dev/null +++ b/scripts/test-patches-local.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +# test-patches-local.sh — Thin wrapper around test-patches.js. +# Primarily exists for CI/bash environments. +# On Windows, call `node scripts/test-patches.js` directly. +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +exec node "$SCRIPT_DIR/test-patches.js" "$@" diff --git a/scripts/test-patches.js b/scripts/test-patches.js new file mode 100644 index 0000000..4030e4e --- /dev/null +++ b/scripts/test-patches.js @@ -0,0 +1,160 @@ +#!/usr/bin/env node +/** + * test-patches.js — Integration tests for the patch framework (T02). + * Tests all 8 manifest entries, dry-run, strict mode, and sabotage detection. + * Cross-platform: works on Windows, macOS, Linux without bash dependency. + */ +const { execSync } = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +const PROJECT_DIR = path.resolve(__dirname, '..'); +const PATCH_JS = path.join(PROJECT_DIR, 'scripts', 'patch.js'); +const FIXTURES = path.join(PROJECT_DIR, 'test', 'fixtures'); +const RESTORE_JS = path.join(__dirname, 'restore-fixtures.js'); + +let PASS = 0; +let FAIL = 0; + +function green(s) { console.log(`\x1b[32m ✓ ${s}\x1b[0m`); PASS++; } +function red(s) { console.log(`\x1b[31m ✗ ${s}\x1b[0m`); FAIL++; } + +function run(cmd, opts = {}) { + try { + const result = execSync(cmd + ' 2>&1', { + cwd: PROJECT_DIR, + encoding: 'utf8', + stdio: ['pipe', 'pipe', 'pipe'], + timeout: 30000, + ...opts, + }); + return { stdout: result.toString(), stderr: '', combined: result.toString(), exitCode: 0 }; + } catch (e) { + const combined = (e.stdout || '') + (e.stderr || ''); + return { + stdout: (e.stdout || '').toString(), + stderr: (e.stderr || '').toString(), + combined: combined.toString(), + exitCode: e.status || 1, + }; + } +} + +function restoreFixtures() { + execSync(`node "${RESTORE_JS}"`, { cwd: PROJECT_DIR, stdio: 'pipe' }); +} + +console.log('=== T02 + T03: Patch Framework Integration Tests ===\n'); + +// ── Setup ── +console.log('[Setup] Restoring fixtures...'); +restoreFixtures(); +green('fixtures restored'); + +// ── Test 1: dry-run + strict → exit 0, 8 entries match exactly once ── +console.log('\n[Test 1] Dry-run + strict (all 8 entries)'); +const dryRun = run(`node "${PATCH_JS}" --repo "${FIXTURES}" --dry-run --strict`); +const matchCount = (dryRun.stdout.match(/finder matches 1 time/g) || []).length; +if (matchCount === 8) green(`8 finders match exactly once`); else red(`expected 8 matches, got ${matchCount}`); +if (dryRun.exitCode === 0) green(`dry-run exits 0`); else red(`dry-run exits ${dryRun.exitCode}`); + +// ── Test 2: strict apply → exit 0, 8 [PASS] lines ── +console.log('\n[Test 2] Strict mode apply (all 8 entries)'); +restoreFixtures(); +const strictRun = run(`node "${PATCH_JS}" --repo "${FIXTURES}" --strict`); +const passCount = (strictRun.stdout.match(/\[PASS\]/g) || []).length; +if (passCount === 8) green(`8 [PASS] lines`); else red(`expected 8 [PASS], got ${passCount}`); +if (strictRun.exitCode === 0) green(`strict mode exits 0`); else red(`strict mode exits ${strictRun.exitCode}`); + +// ── Test 3: verbose shows find/replace/verify steps ── +console.log('\n[Test 3] Verbose mode output'); +restoreFixtures(); +const verboseRun = run(`node "${PATCH_JS}" --repo "${FIXTURES}" --strict --verbose`); +const verboseOut = verboseRun.combined; +if (verboseOut.includes('finder matches')) green(`verbose shows finder steps`); else red(`verbose missing finder detail`); +if (verboseOut.includes('verify matches')) green(`verbose shows verify steps`); else red(`verbose missing verify detail`); + +// ── Test 4: sabotage detection ── +console.log('\n[Test 4] Sabotage detection'); +restoreFixtures(); +const usageFile = path.join(FIXTURES, 'newIDE/app/src/Utils/GDevelopServices/Usage.js'); +const original = fs.readFileSync(usageFile, 'utf8'); +const sabotaged = original.replace('export const hasValidSubscriptionPlan', 'export const hasValidSubscriptionPlan_RENAMED'); +fs.writeFileSync(usageFile, sabotaged, 'utf8'); +const sabotageRun = run(`node "${PATCH_JS}" --repo "${FIXTURES}" --strict`); +fs.writeFileSync(usageFile, original, 'utf8'); // restore +if (sabotageRun.combined.includes('PATCH_FAIL')) { + green(`structured error PATCH_FAIL produced`); +} else { + red(`no PATCH_FAIL in output`); +} +if (sabotageRun.exitCode === 1) green(`sabotage exits 1`); else red(`sabotage exits ${sabotageRun.exitCode}`); + +// ── Test 5: missing manifest → exit 2 ── +console.log('\n[Test 5] Missing manifest file'); +const missingRun = run(`node "${PATCH_JS}" --repo "${FIXTURES}" --manifest /nonexistent/path.json`); +if (missingRun.stdout.toLowerCase().includes('cannot read') || missingRun.stderr.toLowerCase().includes('cannot read')) { + green(`reports cannot read manifest`); +} else { + red(`missing expected error message`); +} +if (missingRun.exitCode === 2) green(`missing manifest exits 2`); else red(`missing manifest exits ${missingRun.exitCode}`); + +// ── T03 Tests ────────────────────────────────────────────────────────── + +const SABOTAGED_DIR = path.join(PROJECT_DIR, 'test', 'fixtures-sabotaged'); +const SABOTAGED_DUP_DIR = path.join(PROJECT_DIR, 'test', 'fixtures-sabotaged-dup'); +const CREATE_SABOTAGED = path.join(__dirname, 'create-sabotaged-fixtures.js'); + +// ── Test 6: sabotaged fixture (function renamed) → PATCH_FAIL exact format ── +console.log('\n[Test 6] T03: Sabotaged fixtures — PATCH_FAIL exact format'); +execSync(`node "${CREATE_SABOTAGED}"`, { cwd: PROJECT_DIR, stdio: 'pipe' }); +const sabotagedRun = run(`node "${PATCH_JS}" --repo "${SABOTAGED_DIR}" --strict`); +if (sabotagedRun.exitCode === 1) green(`sabotaged exits 1`); else red(`sabotaged exits ${sabotagedRun.exitCode}`); +if (sabotagedRun.combined.includes('PATCH_FAIL:P001:newIDE/app/src/Utils/GDevelopServices/Usage.js:finder')) { + green(`exact PATCH_FAIL:P001:newIDE/app/src/Utils/GDevelopServices/Usage.js:finder`); +} else { + red(`missing exact PATCH_FAIL format`); +} +if (!sabotagedRun.combined.includes('[PASS]') || !sabotagedRun.combined.includes('P001')) { + // P001 should NOT show [PASS] + const p001pass = sabotagedRun.combined.match(/\[PASS\].*P001/); + if (!p001pass) green(`P001 does NOT show [PASS]`); else red(`P001 incorrectly shows [PASS]`); +} else { + green(`P001 does NOT show [PASS] (verified)`); +} +// Other 7 patches should still pass +const otherPasses = (sabotagedRun.combined.match(/\[PASS\]/g) || []).length; +if (otherPasses === 7) green(`7 other patches [PASS]`); else red(`expected 7 [PASS], got ${otherPasses}`); + +// ── Test 7: sabotaged-dup fixture (function duplicated) → PATCH_FAIL ── +console.log('\n[Test 7] T03: Duplicate finder — PATCH_FAIL for 2+ matches'); +const dupRun = run(`node "${PATCH_JS}" --repo "${SABOTAGED_DUP_DIR}" --strict`); +if (dupRun.exitCode === 1) green(`dup-sabotaged exits 1`); else red(`dup-sabotaged exits ${dupRun.exitCode}`); +if (dupRun.combined.includes('PATCH_FAIL:P001:newIDE/app/src/Utils/GDevelopServices/Usage.js:finder')) { + green(`exact PATCH_FAIL for duplicate match`); +} else { + red(`missing PATCH_FAIL for duplicate`); +} +// Verify "2 time(s)" appears in error detail +if (dupRun.combined.includes('2 time(s)')) { + green(`reports "2 time(s)" for duplicate`); +} else { + red(`missing "2 time(s)" detail`); +} + +// ── Test 8: --verbose with sabotaged fixtures shows step-level detail ── +console.log('\n[Test 8] T03: Verbose sabotaged shows finder match count 0'); +const verboseSabotaged = run(`node "${PATCH_JS}" --repo "${SABOTAGED_DIR}" --strict --verbose`); +if (verboseSabotaged.combined.includes('finder matches = 0')) { + green(`verbose confirms 0 finder matches`); +} else { + red(`verbose missing finder match count`); +} + +// ── Cleanup ── +console.log('\n[Cleanup] Restoring fixtures...'); +restoreFixtures(); + +console.log(`\n=== Results: ${PASS} passed, ${FAIL} failed ===`); +if (FAIL > 0) process.exit(1); diff --git a/test/fixtures-sabotaged-dup/Core/GDCore/Project/Project.cpp b/test/fixtures-sabotaged-dup/Core/GDCore/Project/Project.cpp new file mode 100644 index 0000000..31eca01 --- /dev/null +++ b/test/fixtures-sabotaged-dup/Core/GDCore/Project/Project.cpp @@ -0,0 +1,7 @@ +// Fixture: Core/GDCore/Project/Project.cpp +// Contains the exact pattern for patch 8 (C++ watermark defaults). + +void Project::SetDefaults() { + SetShowWatermark(false); // BYOK PATCH + showWatermark = true; +} diff --git a/test/fixtures-sabotaged-dup/GDJS/Runtime/index.html b/test/fixtures-sabotaged-dup/GDJS/Runtime/index.html new file mode 100644 index 0000000..2327fa8 --- /dev/null +++ b/test/fixtures-sabotaged-dup/GDJS/Runtime/index.html @@ -0,0 +1,9 @@ + + + diff --git a/test/fixtures-sabotaged-dup/newIDE/app/src/ExportAndShare/ShareDialog/DummyDialog.js b/test/fixtures-sabotaged-dup/newIDE/app/src/ExportAndShare/ShareDialog/DummyDialog.js new file mode 100644 index 0000000..fe076a4 --- /dev/null +++ b/test/fixtures-sabotaged-dup/newIDE/app/src/ExportAndShare/ShareDialog/DummyDialog.js @@ -0,0 +1,5 @@ +// Fixture: newIDE/app/src/ExportAndShare/ShareDialog/DummyDialog.js +// Contains the exact pattern for patch 6 (build quota removal). + +const limitReached = false; /* BYOK PATCH: remove build quota */ +const quotaCheck = current > max; diff --git a/test/fixtures-sabotaged-dup/newIDE/app/src/ProjectCreation/ProjectPropertiesDialog.js b/test/fixtures-sabotaged-dup/newIDE/app/src/ProjectCreation/ProjectPropertiesDialog.js new file mode 100644 index 0000000..b90f695 --- /dev/null +++ b/test/fixtures-sabotaged-dup/newIDE/app/src/ProjectCreation/ProjectPropertiesDialog.js @@ -0,0 +1,9 @@ +// Fixture: newIDE/app/src/ProjectCreation/ProjectPropertiesDialog.js +// Contains the exact pattern for patch 5 (watermark toggle unlock). + +const canRemoveWatermark = true; // BYOK PATCH: always allow watermark removal +const showWatermarkOption = canRemoveWatermark && !isFreePlan; + +export default function ProjectPropertiesDialog() { + return null; +} diff --git a/test/fixtures-sabotaged-dup/newIDE/app/src/Utils/GDevelopServices/ApiConfigs.js b/test/fixtures-sabotaged-dup/newIDE/app/src/Utils/GDevelopServices/ApiConfigs.js new file mode 100644 index 0000000..b218715 --- /dev/null +++ b/test/fixtures-sabotaged-dup/newIDE/app/src/Utils/GDevelopServices/ApiConfigs.js @@ -0,0 +1,12 @@ +// Fixture: newIDE/app/src/Utils/GDevelopServices/ApiConfigs.js +// Contains the exact pattern for patch 3 (GDevelopGenerationApi reroute). + +const isDev = process.env.NODE_ENV === 'development'; + +export const GDevelopGenerationApi = { + baseUrl: typeof window !== 'undefined' && window.__BYOK_API_BASE_URL__ + ? window.__BYOK_API_BASE_URL__ + : ((isDev + ? 'https://api-dev.gdevelop.io/generation' + : 'https://api.gdevelop.io/generation'): string), // BYOK PATCH: dynamic override +}; diff --git a/test/fixtures-sabotaged-dup/newIDE/app/src/Utils/GDevelopServices/Usage.js b/test/fixtures-sabotaged-dup/newIDE/app/src/Utils/GDevelopServices/Usage.js new file mode 100644 index 0000000..10c49e9 --- /dev/null +++ b/test/fixtures-sabotaged-dup/newIDE/app/src/Utils/GDevelopServices/Usage.js @@ -0,0 +1,50 @@ +// Fixture: newIDE/app/src/Utils/GDevelopServices/Usage.js +// Contains the exact patterns for patches 1 (hasValidSubscriptionPlan) and 2 (getUserLimits). + +export const hasValidSubscriptionPlan = ( + subscription: ?Subscription +): boolean => { + const hasValidSubscription = !!subscription && !!subscription.planId + && (!subscription.redemptionCodeValidUntil || // No redemption code + subscription.redemptionCodeValidUntil > Date.now()); // Redemption code is still valid + if (hasValidSubscription) { + // The user has a subscription registered in the backend (classic "Registered" user). + return true; + } + return false; +}; + +// DUPLICATE: second copy of hasValidSubscriptionPlan for sabotage testing +export const hasValidSubscriptionPlan = ( + subscription: ?Subscription +): boolean => { + return false; +}; + +export const getUserLimits = async ( + getAuthorizationHeader: () => Promise, + userId: string +): Promise => { + // BYOK PATCH: return maximum limits + return { + 'gdevelop.maxGames': 9999, + 'gdevelop.maxCloudProjects': 9999, + 'gdevelop.maxBuildsPerDay': 9999, + 'gdevelop.maxBuildsPerMonth': 9999, + 'gdevelop.maxBuildsTotal': 9999, + 'gdevelop.maxExtensions': 9999, + 'gdevelop.maxLeaderboards': 9999, + 'gdevelop.maxMultiplayerGames': 9999, + 'gdevelop.maxTextToSpeech': 9999, + 'gdevelop.maxAITokens': 9999, + 'gdevelop.maxAIProjects': 9999, + 'gdevelop.maxCloudBuilds': 9999, + 'gdevelop.maxEvents': 9999, + 'gdevelop.maxObjects': 9999, + 'gdevelop.maxLayers': 9999, + 'gdevelop.maxScenes': 9999, + 'gdevelop.maxExternalLayouts': 9999, + 'gdevelop.maxEffects': 9999, + 'gdevelop.maxAdMob': 9999, + }; +}; diff --git a/test/fixtures-sabotaged-dup/newIDE/electron-app/app/scripts/preload.js b/test/fixtures-sabotaged-dup/newIDE/electron-app/app/scripts/preload.js new file mode 100644 index 0000000..5db0305 --- /dev/null +++ b/test/fixtures-sabotaged-dup/newIDE/electron-app/app/scripts/preload.js @@ -0,0 +1,12 @@ +// Fixture: newIDE/electron-app/app/scripts/preload.js +// Contains the exact pattern for patch 4 (BYOK proxy detection injection). + +const { contextBridge, ipcRenderer } = require('electron'); + +// Expose protected methods +// BYOK PATCH: inject proxy detection +if (process.env.BYOK_PROXY === 'true') { + contextBridge.exposeInMainWorld('__BYOK_PROXY__', true); +} +// Preload script for GDevelop electron app +contextBridge.exposeInMainWorld('electronAPI', {}); diff --git a/test/fixtures-sabotaged/Core/GDCore/Project/Project.cpp b/test/fixtures-sabotaged/Core/GDCore/Project/Project.cpp new file mode 100644 index 0000000..31eca01 --- /dev/null +++ b/test/fixtures-sabotaged/Core/GDCore/Project/Project.cpp @@ -0,0 +1,7 @@ +// Fixture: Core/GDCore/Project/Project.cpp +// Contains the exact pattern for patch 8 (C++ watermark defaults). + +void Project::SetDefaults() { + SetShowWatermark(false); // BYOK PATCH + showWatermark = true; +} diff --git a/test/fixtures-sabotaged/GDJS/Runtime/index.html b/test/fixtures-sabotaged/GDJS/Runtime/index.html new file mode 100644 index 0000000..2327fa8 --- /dev/null +++ b/test/fixtures-sabotaged/GDJS/Runtime/index.html @@ -0,0 +1,9 @@ + + + diff --git a/test/fixtures-sabotaged/newIDE/app/src/ExportAndShare/ShareDialog/DummyDialog.js b/test/fixtures-sabotaged/newIDE/app/src/ExportAndShare/ShareDialog/DummyDialog.js new file mode 100644 index 0000000..fe076a4 --- /dev/null +++ b/test/fixtures-sabotaged/newIDE/app/src/ExportAndShare/ShareDialog/DummyDialog.js @@ -0,0 +1,5 @@ +// Fixture: newIDE/app/src/ExportAndShare/ShareDialog/DummyDialog.js +// Contains the exact pattern for patch 6 (build quota removal). + +const limitReached = false; /* BYOK PATCH: remove build quota */ +const quotaCheck = current > max; diff --git a/test/fixtures-sabotaged/newIDE/app/src/ProjectCreation/ProjectPropertiesDialog.js b/test/fixtures-sabotaged/newIDE/app/src/ProjectCreation/ProjectPropertiesDialog.js new file mode 100644 index 0000000..b90f695 --- /dev/null +++ b/test/fixtures-sabotaged/newIDE/app/src/ProjectCreation/ProjectPropertiesDialog.js @@ -0,0 +1,9 @@ +// Fixture: newIDE/app/src/ProjectCreation/ProjectPropertiesDialog.js +// Contains the exact pattern for patch 5 (watermark toggle unlock). + +const canRemoveWatermark = true; // BYOK PATCH: always allow watermark removal +const showWatermarkOption = canRemoveWatermark && !isFreePlan; + +export default function ProjectPropertiesDialog() { + return null; +} diff --git a/test/fixtures-sabotaged/newIDE/app/src/Utils/GDevelopServices/ApiConfigs.js b/test/fixtures-sabotaged/newIDE/app/src/Utils/GDevelopServices/ApiConfigs.js new file mode 100644 index 0000000..b218715 --- /dev/null +++ b/test/fixtures-sabotaged/newIDE/app/src/Utils/GDevelopServices/ApiConfigs.js @@ -0,0 +1,12 @@ +// Fixture: newIDE/app/src/Utils/GDevelopServices/ApiConfigs.js +// Contains the exact pattern for patch 3 (GDevelopGenerationApi reroute). + +const isDev = process.env.NODE_ENV === 'development'; + +export const GDevelopGenerationApi = { + baseUrl: typeof window !== 'undefined' && window.__BYOK_API_BASE_URL__ + ? window.__BYOK_API_BASE_URL__ + : ((isDev + ? 'https://api-dev.gdevelop.io/generation' + : 'https://api.gdevelop.io/generation'): string), // BYOK PATCH: dynamic override +}; diff --git a/test/fixtures-sabotaged/newIDE/app/src/Utils/GDevelopServices/Usage.js b/test/fixtures-sabotaged/newIDE/app/src/Utils/GDevelopServices/Usage.js new file mode 100644 index 0000000..69111ea --- /dev/null +++ b/test/fixtures-sabotaged/newIDE/app/src/Utils/GDevelopServices/Usage.js @@ -0,0 +1,43 @@ +// Fixture: newIDE/app/src/Utils/GDevelopServices/Usage.js +// Contains the exact patterns for patches 1 (hasValidSubscriptionPlan) and 2 (getUserLimits). + +export const hasValidSubscriptionPlan_RENAMED = ( + subscription: ?Subscription +): boolean => { + const hasValidSubscription = !!subscription && !!subscription.planId + && (!subscription.redemptionCodeValidUntil || // No redemption code + subscription.redemptionCodeValidUntil > Date.now()); // Redemption code is still valid + if (hasValidSubscription) { + // The user has a subscription registered in the backend (classic "Registered" user). + return true; + } + return false; +}; + +export const getUserLimits = async ( + getAuthorizationHeader: () => Promise, + userId: string +): Promise => { + // BYOK PATCH: return maximum limits + return { + 'gdevelop.maxGames': 9999, + 'gdevelop.maxCloudProjects': 9999, + 'gdevelop.maxBuildsPerDay': 9999, + 'gdevelop.maxBuildsPerMonth': 9999, + 'gdevelop.maxBuildsTotal': 9999, + 'gdevelop.maxExtensions': 9999, + 'gdevelop.maxLeaderboards': 9999, + 'gdevelop.maxMultiplayerGames': 9999, + 'gdevelop.maxTextToSpeech': 9999, + 'gdevelop.maxAITokens': 9999, + 'gdevelop.maxAIProjects': 9999, + 'gdevelop.maxCloudBuilds': 9999, + 'gdevelop.maxEvents': 9999, + 'gdevelop.maxObjects': 9999, + 'gdevelop.maxLayers': 9999, + 'gdevelop.maxScenes': 9999, + 'gdevelop.maxExternalLayouts': 9999, + 'gdevelop.maxEffects': 9999, + 'gdevelop.maxAdMob': 9999, + }; +}; diff --git a/test/fixtures-sabotaged/newIDE/electron-app/app/scripts/preload.js b/test/fixtures-sabotaged/newIDE/electron-app/app/scripts/preload.js new file mode 100644 index 0000000..5db0305 --- /dev/null +++ b/test/fixtures-sabotaged/newIDE/electron-app/app/scripts/preload.js @@ -0,0 +1,12 @@ +// Fixture: newIDE/electron-app/app/scripts/preload.js +// Contains the exact pattern for patch 4 (BYOK proxy detection injection). + +const { contextBridge, ipcRenderer } = require('electron'); + +// Expose protected methods +// BYOK PATCH: inject proxy detection +if (process.env.BYOK_PROXY === 'true') { + contextBridge.exposeInMainWorld('__BYOK_PROXY__', true); +} +// Preload script for GDevelop electron app +contextBridge.exposeInMainWorld('electronAPI', {}); diff --git a/test/fixtures/Core/GDCore/Project/Project.cpp b/test/fixtures/Core/GDCore/Project/Project.cpp new file mode 100644 index 0000000..31eca01 --- /dev/null +++ b/test/fixtures/Core/GDCore/Project/Project.cpp @@ -0,0 +1,7 @@ +// Fixture: Core/GDCore/Project/Project.cpp +// Contains the exact pattern for patch 8 (C++ watermark defaults). + +void Project::SetDefaults() { + SetShowWatermark(false); // BYOK PATCH + showWatermark = true; +} diff --git a/test/fixtures/GDJS/Runtime/index.html b/test/fixtures/GDJS/Runtime/index.html new file mode 100644 index 0000000..2327fa8 --- /dev/null +++ b/test/fixtures/GDJS/Runtime/index.html @@ -0,0 +1,9 @@ + + + diff --git a/test/fixtures/newIDE/app/src/ExportAndShare/ShareDialog/DummyDialog.js b/test/fixtures/newIDE/app/src/ExportAndShare/ShareDialog/DummyDialog.js new file mode 100644 index 0000000..fe076a4 --- /dev/null +++ b/test/fixtures/newIDE/app/src/ExportAndShare/ShareDialog/DummyDialog.js @@ -0,0 +1,5 @@ +// Fixture: newIDE/app/src/ExportAndShare/ShareDialog/DummyDialog.js +// Contains the exact pattern for patch 6 (build quota removal). + +const limitReached = false; /* BYOK PATCH: remove build quota */ +const quotaCheck = current > max; diff --git a/test/fixtures/newIDE/app/src/ProjectCreation/ProjectPropertiesDialog.js b/test/fixtures/newIDE/app/src/ProjectCreation/ProjectPropertiesDialog.js new file mode 100644 index 0000000..b90f695 --- /dev/null +++ b/test/fixtures/newIDE/app/src/ProjectCreation/ProjectPropertiesDialog.js @@ -0,0 +1,9 @@ +// Fixture: newIDE/app/src/ProjectCreation/ProjectPropertiesDialog.js +// Contains the exact pattern for patch 5 (watermark toggle unlock). + +const canRemoveWatermark = true; // BYOK PATCH: always allow watermark removal +const showWatermarkOption = canRemoveWatermark && !isFreePlan; + +export default function ProjectPropertiesDialog() { + return null; +} diff --git a/test/fixtures/newIDE/app/src/Utils/GDevelopServices/ApiConfigs.js b/test/fixtures/newIDE/app/src/Utils/GDevelopServices/ApiConfigs.js new file mode 100644 index 0000000..b218715 --- /dev/null +++ b/test/fixtures/newIDE/app/src/Utils/GDevelopServices/ApiConfigs.js @@ -0,0 +1,12 @@ +// Fixture: newIDE/app/src/Utils/GDevelopServices/ApiConfigs.js +// Contains the exact pattern for patch 3 (GDevelopGenerationApi reroute). + +const isDev = process.env.NODE_ENV === 'development'; + +export const GDevelopGenerationApi = { + baseUrl: typeof window !== 'undefined' && window.__BYOK_API_BASE_URL__ + ? window.__BYOK_API_BASE_URL__ + : ((isDev + ? 'https://api-dev.gdevelop.io/generation' + : 'https://api.gdevelop.io/generation'): string), // BYOK PATCH: dynamic override +}; diff --git a/test/fixtures/newIDE/app/src/Utils/GDevelopServices/Usage.js b/test/fixtures/newIDE/app/src/Utils/GDevelopServices/Usage.js new file mode 100644 index 0000000..a7fc12d --- /dev/null +++ b/test/fixtures/newIDE/app/src/Utils/GDevelopServices/Usage.js @@ -0,0 +1,36 @@ +// Fixture: newIDE/app/src/Utils/GDevelopServices/Usage.js +// Contains the exact patterns for patches 1 (hasValidSubscriptionPlan) and 2 (getUserLimits). + +export const hasValidSubscriptionPlan = ( + subscription: ?Subscription +): boolean => { + return true; // BYOK PATCH: always return valid subscription +}; + +export const getUserLimits = async ( + getAuthorizationHeader: () => Promise, + userId: string +): Promise => { + // BYOK PATCH: return maximum limits + return { + 'gdevelop.maxGames': 9999, + 'gdevelop.maxCloudProjects': 9999, + 'gdevelop.maxBuildsPerDay': 9999, + 'gdevelop.maxBuildsPerMonth': 9999, + 'gdevelop.maxBuildsTotal': 9999, + 'gdevelop.maxExtensions': 9999, + 'gdevelop.maxLeaderboards': 9999, + 'gdevelop.maxMultiplayerGames': 9999, + 'gdevelop.maxTextToSpeech': 9999, + 'gdevelop.maxAITokens': 9999, + 'gdevelop.maxAIProjects': 9999, + 'gdevelop.maxCloudBuilds': 9999, + 'gdevelop.maxEvents': 9999, + 'gdevelop.maxObjects': 9999, + 'gdevelop.maxLayers': 9999, + 'gdevelop.maxScenes': 9999, + 'gdevelop.maxExternalLayouts': 9999, + 'gdevelop.maxEffects': 9999, + 'gdevelop.maxAdMob': 9999, + }; +}; diff --git a/test/fixtures/newIDE/electron-app/app/scripts/preload.js b/test/fixtures/newIDE/electron-app/app/scripts/preload.js new file mode 100644 index 0000000..5db0305 --- /dev/null +++ b/test/fixtures/newIDE/electron-app/app/scripts/preload.js @@ -0,0 +1,12 @@ +// Fixture: newIDE/electron-app/app/scripts/preload.js +// Contains the exact pattern for patch 4 (BYOK proxy detection injection). + +const { contextBridge, ipcRenderer } = require('electron'); + +// Expose protected methods +// BYOK PATCH: inject proxy detection +if (process.env.BYOK_PROXY === 'true') { + contextBridge.exposeInMainWorld('__BYOK_PROXY__', true); +} +// Preload script for GDevelop electron app +contextBridge.exposeInMainWorld('electronAPI', {});