"use strict"; const test = require("node:test"); const assert = require("node:assert/strict"); const workflow = require("../../Web/legacy-foreign-index-candle-workflow.js"); const profiles = [ ["다우존스", "Dow", "DJI@DJI"], ["나스닥", "Nasdaq", "NAS@IXIC"], ["S&P500", "Sp500", "SPI@SPX"] ]; const periods = [ ["5일", 5, "8061"], ["20일", 20, "8040"], ["60일", 60, "8046"], ["120일", 120, "8051"], ["240일", 240, "8056"] ]; function raw(subject = "다우존스", subtype = "5일", itemIndex = 0, enabled = true) { return { itemIndex, enabled, groupCode: "해외지수", subject, graphicType: "캔들그래프", subtype, pageText: "1/1", dataCode: "" }; } function outcome(pending) { return { itemIndex: pending.itemIndex, status: "resolved", targetKey: pending.targetKey, inputName: pending.inputName, symbol: pending.symbol, nationCode: "US", foreignDomesticCode: "0", sceneCode: "s8010", valueType: "PRICE", periodDays: pending.periodDays, cutCode: pending.cutCode }; } function response(requestId, pendingRows, rows = pendingRows.map(outcome)) { return { requestId, retrievedAt: "2026-07-12T12:34:56+09:00", totalRowCount: rows.length, rows }; } test("all three fixed targets and five periods classify including inactive 240-day contract", () => { let itemIndex = 0; for (const [subject, targetKey, symbol] of profiles) { for (const [legacyPeriod, periodDays, cutCode] of periods) { const pending = workflow.classify(raw(subject, legacyPeriod, itemIndex), { id: `foreign_${itemIndex}`, fadeDuration: 6 }); assert.ok(pending); assert.equal(pending.targetKey, targetKey); assert.equal(pending.symbol, symbol); assert.equal(pending.periodDays, periodDays); assert.equal(pending.cutCode, cutCode); assert.equal(workflow.isPending(pending), true); itemIndex += 1; } } assert.equal(itemIndex, 15); }); test("raw classifier is exact and preserves disabled state", () => { const disabled = workflow.classify(raw("다우존스", "240일", 9, false), { id: "foreign_disabled", fadeDuration: 0 }); assert.ok(disabled); assert.equal(disabled.enabled, false); for (const invalid of [ { ...raw(), groupCode: "FOREIGN_INDEX" }, { ...raw(), subject: "다우" }, { ...raw(), graphicType: "캔들 그래프" }, { ...raw(), subtype: "1년" }, { ...raw(), pageText: "1/2" }, { ...raw(), dataCode: "DJI@DJI|US|0" }, { ...raw(), itemIndex: 1000 } ]) { assert.equal(workflow.classify(invalid, { id: "foreign_bad", fadeDuration: 6 }), null); } assert.equal(workflow.classify(raw(), { id: "foreign_bad", fadeDuration: 6, extra: true }), null); }); test("only an exact correlated native result materializes s8010 PRICE identity", () => { const pending = workflow.classify(raw("S&P500", "120일", 7, false), { id: "foreign_materialized", fadeDuration: 4 }); const normalized = workflow.normalizeResponse( response("load_1", [pending]), "load_1", [pending]); assert.ok(normalized); const entry = workflow.materialize(pending, normalized.rows[0]); assert.ok(entry); assert.equal(workflow.isMaterializedEntry(entry), true); assert.equal(entry.builderKey, "s8010"); assert.equal(entry.code, "8051"); assert.equal(entry.enabled, false); assert.deepEqual(entry.selection, { groupCode: "FOREIGN_INDEX", subject: "S&P500", graphicType: "", subtype: "PRICE", dataCode: "SPI@SPX|US|0" }); assert.equal(entry.operator.source, "legacy-foreign-index-candle-workflow"); }); test("identity, order, count, correlation and result-shape tampering fail closed", () => { const first = workflow.classify(raw("다우존스", "5일", 2), { id: "foreign_first", fadeDuration: 6 }); const second = workflow.classify(raw("나스닥", "20일", 8), { id: "foreign_second", fadeDuration: 6 }); const pending = [first, second]; const exact = response("load_2", pending); assert.ok(workflow.normalizeResponse(exact, "load_2", pending)); assert.equal(workflow.normalizeResponse(exact, "stale_load", pending), null); assert.equal(workflow.normalizeResponse({ ...exact, totalRowCount: 1 }, "load_2", pending), null); assert.equal(workflow.normalizeResponse({ ...exact, rows: [...exact.rows].reverse() }, "load_2", pending), null); assert.equal(workflow.normalizeResponse({ ...exact, extra: true }, "load_2", pending), null); for (const [key, value] of [ ["targetKey", "Nasdaq"], ["inputName", "나스닥"], ["symbol", "NAS@IXIC"], ["nationCode", "KR"], ["foreignDomesticCode", "1"], ["sceneCode", "s5001"], ["valueType", "VOLUME"], ["periodDays", 20], ["cutCode", "8040"] ]) { const rows = [{ ...exact.rows[0], [key]: value }, exact.rows[1]]; assert.equal(workflow.normalizeResponse({ ...exact, rows }, "load_2", pending), null, key); } }); test("per-row native failures remain terminal and cannot materialize", () => { const pending = workflow.classify(raw(), { id: "foreign_failed", fadeDuration: 6 }); for (const failure of [ "INVALID_ROW", "IDENTITY_NOT_FOUND", "IDENTITY_AMBIGUOUS", "DATABASE_DATA_INVALID", "RESTORE_FAILED" ]) { const normalized = workflow.normalizeResponse(response("load_3", [pending], [{ itemIndex: pending.itemIndex, status: "failed", failure }]), "load_3", [pending]); assert.ok(normalized); assert.equal(workflow.materialize(pending, normalized.rows[0]), null); } assert.equal(workflow.normalizeResponse(response("load_3", [pending], [{ itemIndex: pending.itemIndex, status: "failed", failure: "RETRY" }]), "load_3", [pending]), null); }); test("forged pending and forged resolved values never materialize", () => { const pending = workflow.classify(raw(), { id: "foreign_trusted", fadeDuration: 6 }); const normalized = workflow.normalizeResponse(response("load_4", [pending]), "load_4", [pending]); assert.equal(workflow.materialize({ ...pending }, normalized.rows[0]), null); assert.equal(workflow.materialize(pending, { ...normalized.rows[0] }), null); }); test("legacy signature round-trips only the exact verified entry", () => { const pending = workflow.classify(raw("나스닥", "240일", 3, false), { id: "foreign_round_trip", fadeDuration: 5 }); const normalized = workflow.normalizeResponse(response("load_5", [pending]), "load_5", [pending]); const entry = workflow.materialize(pending, normalized.rows[0]); const restored = workflow.restorePlaylistEntry(entry); assert.ok(restored); assert.deepEqual(workflow.legacySelectionForEntry(restored), { groupCode: "해외지수", subject: "나스닥", graphicType: "캔들그래프", subtype: "240일", dataCode: "" }); assert.equal(workflow.matchesRaw(restored, raw("나스닥", "240일", 3, false)), true); assert.equal(workflow.matchesRaw(restored, raw("나스닥", "120일", 3, false)), false); for (const tampered of [ { ...entry, code: "8040" }, { ...entry, selection: { ...entry.selection, subject: "다우존스" } }, { ...entry, operator: { ...entry.operator, symbol: "DJI@DJI" } }, { ...entry, operator: { ...entry.operator, periodDays: 120 } }, { ...entry, extra: true } ]) { assert.equal(workflow.restorePlaylistEntry(tampered), null); } }); test("enabled replacement preserves the branded foreign-index proof immutably", () => { const storedRaw = raw("S&P500", "20일", 6, true); const pending = workflow.classify(storedRaw, { id: "foreign_toggle", fadeDuration: 6 }); const normalized = workflow.normalizeResponse(response("load_toggle", [pending]), "load_toggle", [pending]); const entry = workflow.materialize(pending, normalized.rows[0]); const replacement = workflow.withEnabled(entry, false); 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(replacement, { ...storedRaw, enabled: false }), true); assert.equal(workflow.withEnabled({ ...entry }, false), null); }); test("bridge errors are request-bound and never retryable", () => { const error = { requestId: "load_error", code: "PLAYOUT_PRIORITY", message: "송출 우선 처리로 취소했습니다.", retryable: false }; assert.deepEqual(workflow.normalizeError(error, "load_error"), error); assert.equal(workflow.normalizeError({ ...error, retryable: true }, "load_error"), null); assert.equal(workflow.normalizeError(error, "stale"), null); assert.equal(workflow.normalizeError({ ...error, code: "RETRY" }, "load_error"), null); });