const fs = require('fs'); const path = require('path'); const read = (...p) => fs.readFileSync(path.join(__dirname, '..', ...p), 'utf8'); const assert = (cond, msg) => { if (!cond) throw new Error(msg); }; // ---- Frontend API contract (client.ts) ---- const client = read('frontend', 'src', 'api', 'client.ts'); assert( client.includes('`/platforms/${platformId}/monitored`'), 'client.ts setPlatformMonitored must PUT /platforms/{id}/monitored' ); assert( client.includes("apiClient.get>('/platforms/monitored-counts')"), 'client.ts getPlatformMonitoredCounts must GET /platforms/monitored-counts' ); assert( /export interface PlatformMonitorCount\s*\{[\s\S]*monitorDefault: boolean \| null;/.test(client), 'client.ts must export PlatformMonitorCount with monitorDefault' ); assert( /export interface GameListDto\s*\{[\s\S]*monitored: boolean;[\s\S]*\}/.test(client), 'GameListDto type must carry monitored' ); // ---- Per-game marker on the card (GameCard.tsx) ---- const card = read('frontend', 'src', 'components', 'GameCard.tsx'); assert(card.includes('faBookmark'), 'GameCard must import a bookmark icon'); assert(card.includes('game-card-monitor-btn'), 'GameCard must render the monitor marker button'); assert( card.includes('await monitorApi.setMonitored(game.id, next)'), 'GameCard marker must toggle via monitorApi.setMonitored' ); assert( card.includes('e.stopPropagation()'), 'GameCard marker click must not bubble to card navigation' ); assert(card.includes('onMonitoredChange'), 'GameCard must expose onMonitoredChange'); // ---- Platform toggle: Library filter bar ---- const library = read('frontend', 'src', 'pages', 'Library.tsx'); assert( library.includes('handlePlatformMonitorToggle(selectedPlatformData.id)'), 'Library platform action bar must call handlePlatformMonitorToggle' ); assert( library.includes('monitorApi.setPlatformMonitored('), 'Library must invoke setPlatformMonitored' ); assert( library.includes('onMonitoredChange={handleCardMonitoredChange}'), 'Library grid card must wire onMonitoredChange' ); // ---- Platform toggle: Platforms shelf header ---- const platforms = read('frontend', 'src', 'pages', 'Platforms.tsx'); assert( platforms.includes('shelf__monitor'), 'Platforms shelf header must render the platform monitor toggle' ); assert( platforms.includes('handlePlatformMonitorToggle(shelf.platform.id)'), 'Platforms shelf toggle must call handlePlatformMonitorToggle' ); assert( platforms.includes('monitored: dto.monitored'), 'Platforms dtoToGame must pass monitored through so the card marker shows state' ); // ---- Per-game monitor button on the game detail page ---- const details = read('frontend', 'src', 'pages', 'GameDetails.tsx'); assert( details.includes('monitor-toggle-btn'), 'GameDetails must render a prominent monitor toggle button' ); assert( details.includes('await monitorApi.setMonitored(game.id, next)'), 'GameDetails monitor button must toggle via monitorApi.setMonitored' ); assert( details.includes('const toggleGameMonitored'), 'GameDetails must define the toggleGameMonitored handler' ); // The MonitorPanel checkbox must follow the prop so the header button and the // panel stay consistent. const panel = read('frontend', 'src', 'components', 'MonitorPanel.tsx'); assert( /useEffect\(\(\) => \{ setMonitored\(initialMonitored\); \}, \[initialMonitored\]\);/.test(panel), 'MonitorPanel must sync internal monitored state from initialMonitored' ); // ---- Card marker sits top-left (per user preference) ---- const cardCss = read('frontend', 'src', 'components', 'GameCard.css'); assert( /\.game-card-monitor-btn\s*\{[^}]*top:\s*8px;[^}]*left:\s*8px;/.test(cardCss), 'Card monitor marker must be anchored top-left' ); // ---- Backend route + field contract (static string pins; runs without dotnet) ---- const controller = read('src', 'RetroArr.Api.V3', 'Monitor', 'MonitorController.cs'); assert( controller.includes('[HttpPut("platforms/{platformId:int}/monitored")]'), 'MonitorController must expose PUT platforms/{platformId}/monitored' ); assert( controller.includes('[HttpGet("platforms/monitored-counts")]'), 'MonitorController must expose GET platforms/monitored-counts' ); assert( controller.includes('PlatformService.SetMonitorNewItemsDefault(platformId, request.Monitored)'), 'Platform toggle must persist the per-platform default' ); const platformService = read('src', 'RetroArr.Core', 'Games', 'PlatformService.cs'); assert( platformService.includes('GetMonitorNewItemsDefault(int platformId, bool fallback)'), 'PlatformService must provide GetMonitorNewItemsDefault with a fallback' ); assert( platformService.includes('SetMonitorNewItemsDefault(int platformId, bool monitored)'), 'PlatformService must provide SetMonitorNewItemsDefault' ); assert( platformService.includes('platform_monitoring.json'), 'PlatformService must persist the monitoring default to platform_monitoring.json' ); const scanner = read('src', 'RetroArr.Core', 'Games', 'MediaScannerService.cs'); assert( scanner.includes('finalGame.Monitored = PlatformService.GetMonitorNewItemsDefault(finalGame.PlatformId, false)'), 'New scanned games must inherit the platform monitoring default' ); const dto = read('src', 'RetroArr.Core', 'Games', 'GameListDto.cs'); assert(dto.includes('public bool Monitored { get; set; }'), 'GameListDto must carry Monitored'); const repo = read('src', 'RetroArr.Core', 'Games', 'SqliteGameRepository.cs'); assert(repo.includes('Monitored = g.Monitored'), 'Paged projection must include Monitored'); console.log('monitor-platform-toggle: all contract checks passed');