const test = require("node:test"); const assert = require("node:assert/strict"); const workflow = require("../../Web/named-nxt-theme-restore-workflow.js"); function raw(overrides = {}) { return { itemIndex: 4, enabled: true, groupCode: "테마_NXT", subject: "반도체(NXT)", graphicType: "5단 표그래프", subtype: "테마-현재가(입력순)", pageText: "1/9", dataCode: "0123", ...overrides }; } function resolved(overrides = {}) { return { itemIndex: 4, status: "resolved", actionId: "five-row-current", builderKey: "s5074", cutCode: "5074", pageSize: 5, sort: "INPUT_ORDER", session: "PRE_MARKET", themeTitle: "반도체", storedThemeCode: "0123", currentThemeCode: "87654321", previewItemCount: 7, liveItemCount: 6, previewIsTruncated: false, codeWasRemapped: true, ...overrides }; } function response(row) { return { requestId: "load-1", retrievedAt: "2026-07-12T12:59:00+09:00", totalRowCount: 1, rows: [row] }; } test("classify routes every exact NXT theme group row for native fail-closed verification", () => { const pending = workflow.classify(raw({ subject: "", graphicType: "unsupported", subtype: "", pageText: "", dataCode: "" }), { id: "db-4", fadeDuration: 6 }); assert.equal(workflow.isPending(pending), true); assert.equal(pending.rawSelection.groupCode, "테마_NXT"); assert.equal(workflow.classify(raw({ groupCode: "테마" }), { id: "db-4", fadeDuration: 6 }), null); }); test("normalizeResponse accepts one exact correlated current identity", () => { const pending = workflow.classify(raw(), { id: "db-4", fadeDuration: 6 }); const result = workflow.normalizeResponse(response(resolved()), "load-1", [pending]); assert.equal(result.rows[0].currentThemeCode, "87654321"); assert.equal(result.rows[0].liveItemCount, 6); }); test("normalizeResponse rejects expanded, stale, uncorrelated, or impossible identities", () => { const pending = workflow.classify(raw(), { id: "db-4", fadeDuration: 6 }); assert.equal(workflow.normalizeResponse(response({ ...resolved(), extra: true }), "load-1", [pending]), null); assert.equal(workflow.normalizeResponse(response(resolved({ storedThemeCode: "999" })), "load-1", [pending]), null); assert.equal(workflow.normalizeResponse(response(resolved({ themeTitle: "다른제목" })), "load-1", [pending]), null); assert.equal(workflow.normalizeResponse(response(resolved({ liveItemCount: 8 })), "load-1", [pending]), null); assert.equal(workflow.normalizeResponse(response(resolved({ previewItemCount: 241, previewIsTruncated: false })), "load-1", [pending]), null); }); test("failed rows preserve a closed no-retry reason", () => { const pending = workflow.classify(raw(), { id: "db-4", fadeDuration: 6 }); for (const failure of [ "INVALID_ROW", "UNSUPPORTED_ACTION", "IDENTITY_NOT_FOUND", "IDENTITY_AMBIGUOUS", "PREVIEW_EMPTY", "LIVE_ITEMS_EMPTY", "DATABASE_DATA_INVALID", "RESTORE_FAILED" ]) { const result = workflow.normalizeResponse(response({ itemIndex: 4, status: "failed", failure }), "load-1", [pending]); assert.equal(result.rows[0].failure, failure); } assert.equal(workflow.normalizeError({ requestId: "load-1", code: "DATABASE_UNAVAILABLE", message: "재검증 실패", retryable: false }, "load-1").retryable, false); }); test("materialize uses only the current code while retaining the old raw identity for lossless save", () => { const sourceRaw = raw({ enabled: false }); const pending = workflow.classify(sourceRaw, { id: "db-4", fadeDuration: 6 }); const outcome = workflow.normalizeResponse(response(resolved()), "load-1", [pending]).rows[0]; const entry = workflow.materialize(pending, outcome); assert.equal(workflow.isMaterializedEntry(entry), true); assert.equal(entry.enabled, false); assert.equal(entry.selection.groupCode, "PAGED_NXT_THEME"); assert.equal(entry.selection.dataCode, "87654321"); assert.equal(entry.operator.theme.themeCode, "87654321"); assert.equal(entry.pagePreview.itemCount, 6); assert.deepEqual(workflow.legacySelectionForEntry(entry), { groupCode: "테마_NXT", subject: "반도체(NXT)", graphicType: "5단 표그래프", subtype: "테마-현재가(입력순)", dataCode: "0123" }); assert.equal(workflow.matchesRaw(entry, sourceRaw), true); }); test("dynamic NXT session changes exactly at local 13:00 without losing native proof", () => { const sourceRaw = raw(); const pending = workflow.classify(sourceRaw, { id: "db-4", fadeDuration: 6 }); const outcome = workflow.normalizeResponse(response(resolved()), "load-1", [pending]).rows[0]; const entry = workflow.materialize(pending, outcome); const atTwelve = workflow.refreshDynamicSession(entry, new Date(2026, 6, 12, 12, 59)); const atThirteen = workflow.refreshDynamicSession(atTwelve, new Date(2026, 6, 12, 13, 0)); assert.equal(atTwelve.selection.graphicType, "PRE_MARKET"); assert.equal(atThirteen.selection.graphicType, "AFTER_MARKET"); assert.equal(workflow.isMaterializedEntry(atThirteen), true); assert.equal(workflow.matchesRaw(atThirteen, sourceRaw), true); }); test("enabled replacement is immutable and retains NXT proof across session refresh", () => { const sourceRaw = raw({ enabled: true }); const pending = workflow.classify(sourceRaw, { id: "db-toggle", fadeDuration: 6 }); const outcome = workflow.normalizeResponse(response(resolved({ itemIndex: 4 })), "load-1", [pending]).rows[0]; const entry = workflow.materialize(pending, outcome); const replacement = workflow.withEnabled(entry, false); const refreshed = workflow.refreshDynamicSession(replacement, new Date(2026, 6, 12, 13, 0)); assert.notEqual(replacement, entry); assert.equal(Object.isFrozen(replacement), true); assert.equal(entry.enabled, true); assert.equal(replacement.enabled, false); assert.equal(workflow.isMaterializedEntry(replacement), true); assert.equal(workflow.matchesRaw(refreshed, { ...sourceRaw, enabled: false }), true); assert.equal(workflow.withEnabled({ ...entry }, false), null); }); test("materialization refuses a failed outcome and non-pending objects", () => { const pending = workflow.classify(raw(), { id: "db-4", fadeDuration: 6 }); assert.equal(workflow.materialize(pending, { itemIndex: 4, status: "failed", failure: "LIVE_ITEMS_EMPTY" }), null); assert.equal(workflow.materialize({}, resolved()), null); });