"use strict"; const test = require("node:test"); const assert = require("node:assert/strict"); const fs = require("node:fs"); const path = require("node:path"); const root = path.resolve(__dirname, "..", ".."); const app = fs.readFileSync(path.join(root, "Web", "app.js"), "utf8"); const index = fs.readFileSync(path.join(root, "Web", "index.html"), "utf8"); const native = fs.readFileSync(path.join(root, "MainWindow.NamedKrxThemeRestore.cs"), "utf8"); const namedBridge = fs.readFileSync(path.join(root, "MainWindow.NamedPlaylists.cs"), "utf8"); function functionBody(name, nextName) { const start = app.indexOf(`function ${name}(`); const end = app.indexOf(`function ${nextName}(`, start + 1); assert.ok(start >= 0 && end > start, `${name} must precede ${nextName}`); return app.slice(start, end); } test("named KRX restore is correlated from one native load through page preflight", () => { assert.match(index, /src="named-krx-theme-restore-workflow\.js"[\s\S]*src="named-nxt-theme-restore-workflow\.js"/u); assert.match(namedBridge, /await PostNamedKrxThemeRestoreAsync\([\s\S]*await PostNamedNxtThemeRestoreAsync\(/u); assert.match(native, /PostMessage\("named-krx-theme-restore-results"/u); assert.match(app, /krxThemeRestorePending/u); assert.match(app, /case "named-krx-theme-restore-results"/u); assert.match(app, /case "named-krx-theme-restore-error"/u); const finalize = functionBody("finalizeNamedManualRestores", "handleNamedManualListResults"); assert.match(finalize, /state\.namedPlaylist\.krxThemeRestorePending/u); assert.match(finalize, /prepareNamedPagePreflight/u); }); test("generic theme restore cannot trust a historical KRX code before native verification", () => { const addTheme = functionBody("addThemeNamedCandidates", "addOverseasNamedCandidates"); assert.match(addTheme, /rawRow\.groupCode === "테마" \|\| rawRow\.groupCode === "테마_NXT"/u); const load = functionBody("handleNamedPlaylistLoadResults", "failNamedComparisonRestore"); assert.match(load, /namedKrxThemeRestoreWorkflow\.classify/u); assert.match(load, /pendingKrxThemes\.length === 0/u); assert.match(load, /krxThemeRestorePending = pendingKrxThemes\.length/u); const resolve = functionBody("resolveNamedPlaylistRawRow", "prepareNamedPagePreflight"); assert.match(resolve, /rawRow\?\.groupCode === "테마" \|\| rawRow\?\.groupCode === "테마_NXT"[\s\S]*return null/u); }); test("current KRX identity stays branded in memory and old code is retained only for save", () => { const materialize = functionBody("materializeNamedRestoration", "namedPlaylistPrepareBlockers"); assert.match(materialize, /namedKrxThemeRestoreWorkflow\.isMaterializedEntry\(row\.entry\)/u); const synchronize = functionBody("synchronizeThemeEntries", "clearOverseasTimeout"); assert.match(synchronize, /namedKrxThemeRestoreWorkflow\.isMaterializedEntry\(item\)[\s\S]*\? item/u); const fields = functionBody("namedLegacyFieldsForEntry", "isTrustedNamedPlaylistEntry"); assert.match(fields, /namedKrxThemeRestoreWorkflow\.legacySelectionForEntry\(entry\)/u); assert.match(fields, /namedKrxThemeRestoreWorkflow\.matchesRaw\(entry, rawRow\)/u); assert.match(fields, /KRX 테마의 과거 code와 현재 송출 code/u); }); test("KRX failures are terminal and never trigger an automatic retry or playout", () => { const failed = functionBody("failNamedKrxThemeRestore", "handleNamedKrxThemeRestoreResults"); assert.match(failed, /krxThemeRestorePending = null/u); assert.doesNotMatch(app, /retryNamedKrxTheme|requestNamedKrxThemeRestore/u); assert.doesNotMatch(native, /IPlayoutEngine|PrepareAsync|TakeInAsync|NextAsync|TakeOutAsync/u); assert.match(native, /retryable = false/u); }); test("KRX success accounting and post-load cancellation close every correlated busy record", () => { const results = functionBody("handleNamedKrxThemeRestoreResults", "handleNamedKrxThemeRestoreError"); assert.match(results, /state\.playlist\[pending\.itemIndex\] = entry;\s*restoredCount \+= 1;/u); const clear = functionBody("clearNamedPlaylistLoadState", "saveNamedPlaylistBackup"); assert.match(clear, /krxThemeRestorePending = null/u); const busy = functionBody("namedPlaylistBusy", "renderNamedPlaylist"); assert.match(busy, /state\.namedPlaylist\.krxThemeRestorePending/u); const loadError = functionBody("handleNamedPlaylistError", "normalizePlayoutState"); assert.match(loadError, /error\.operation !== "load"/u); assert.match(loadError, /state\.namedPlaylist\.krxThemeRestorePending\?\.requestId === error\.requestId[\s\S]*krxThemeRestorePending = null/u); assert.match(loadError, /복원 배치는 자동 재시도하지 않습니다/u); });