"use strict"; const test = require("node:test"); const assert = require("node:assert/strict"); const workflow = require("../../Web/candle-options-workflow.js"); const stock = require("../../Web/operator-workflow.js"); const fixed = require("../../Web/legacy-fixed-workflow.js"); const industry = require("../../Web/industry-workflow.js"); const overseas = require("../../Web/overseas-workflow.js"); const namedForeignStock = require("../../Web/named-foreign-stock-restore-workflow.js"); const tradingHalt = require("../../Web/trading-halt-workflow.js"); const adapters = { stock, fixed, industry, overseas, tradingHalt }; const options = id => ({ id, fadeDuration: 6, ma5: false, ma20: false }); const domesticStock = Object.freeze({ market: "kospi", stockName: "삼성전자", displayName: "삼성전자", stockCode: "005930", isNxt: false }); const industryItem = Object.freeze({ market: "kospi", name: "전기전자", code: "013" }); const overseasStock = Object.freeze({ inputName: "NVIDIA", symbol: "NAS@NVDA", nationCode: "US", fdtc: "1" }); const haltedStock = Object.freeze({ market: "kospi", stockCode: "005930", displayName: "삼성전자" }); function entries() { const fixedAction = fixed.actions.find(value => value.builderKey === "s8010" && value.available); const industryCut = industry.cuts.find(value => value.kind === "candle"); const pendingNamedForeign = namedForeignStock.classify({ itemIndex: 0, enabled: false, groupCode: "해외종목", subject: "NVIDIA", graphicType: "캔들그래프", subtype: "120일", pageText: "1/1", dataCode: "" }, options("named-overseas-candle")); const namedResponse = namedForeignStock.normalizeResponse({ requestId: "named-overseas-load", retrievedAt: "2026-07-12T12:34:56+09:00", totalRowCount: 1, rows: [{ itemIndex: 0, status: "resolved", actionId: "stock-candle-120d", inputName: "NVIDIA", symbol: "NAS@NVDA", nationCode: "US", foreignDomesticCode: "1", builderKey: "s8010", cutCode: "8051", valueType: "PRICE", periodDays: 120 }] }, "named-overseas-load", [pendingNamedForeign]); const namedForeignEntry = namedForeignStock.materialize( pendingNamedForeign, namedResponse.rows[0]); return [ stock.createStockPlaylistEntry(domesticStock, "candle-120d", options("stock-candle")), fixed.createFixedPlaylistEntry(fixedAction.id, options("fixed-candle")), industry.createIndustryPlaylistEntry("kospi", industryCut.id, { ...options("industry-candle"), selected: industryItem }), overseas.createOverseasStockPlaylistEntry( overseasStock, "stock-candle-120d", options("overseas-candle")), namedForeignEntry, tradingHalt.createTradingHaltPlaylistEntry( haltedStock, "candle-120d", options("halt-candle")) ]; } test("global flags map to the exact closed candle graphic type", () => { assert.equal(workflow.expectedGraphicType(false, false), ""); assert.equal(workflow.expectedGraphicType(true, false), "MA5"); assert.equal(workflow.expectedGraphicType(false, true), "MA20"); assert.equal(workflow.expectedGraphicType(true, true), "MA5,MA20"); assert.throws(() => workflow.expectedGraphicType("yes", true)); }); test("all original and DB-restored candle entry sources can be recreated with global flags", () => { for (const entry of entries()) { const recreated = workflow.recreateCandleEntry(entry, true, true, adapters); assert.ok(recreated, entry.operator.source); assert.equal(recreated.selection.graphicType, "MA5,MA20"); assert.deepEqual(recreated.operator.movingAverages, { ma5: true, ma20: true }); assert.equal(recreated.enabled, entry.enabled); if (entry.operator.source === namedForeignStock.source) { assert.equal(recreated.operator.source, namedForeignStock.source); assert.deepEqual(recreated.operator.rawSelection, entry.operator.rawSelection); assert.ok(namedForeignStock.restorePlaylistEntry(recreated)); assert.deepEqual(namedForeignStock.legacySelectionForEntry(recreated), { groupCode: "해외종목", subject: "NVIDIA", graphicType: "캔들그래프", subtype: "120일", dataCode: "" }); assert.equal(namedForeignStock.matchesRaw(recreated, { itemIndex: 0, enabled: false, groupCode: "해외종목", subject: "NVIDIA", graphicType: "캔들그래프", subtype: "120일", pageText: "1/1", dataCode: "" }), true); } } }); test("playlist synchronization changes only s8010 entries and preserves order", () => { const candles = entries(); const nonCandle = stock.createStockPlaylistEntry(domesticStock, "basic-current", { id: "non-candle", fadeDuration: 6, ma5: false, ma20: false }); const input = [candles[0], nonCandle, ...candles.slice(1)]; const result = workflow.synchronizePlaylist(input, true, false, adapters); assert.equal(result.valid, true); assert.equal(result.changed, true); assert.equal(result.playlist[1], nonCandle); assert.deepEqual(result.playlist.map(value => value.id), input.map(value => value.id)); for (const entry of result.playlist.filter(value => value.builderKey === "s8010")) { assert.equal(entry.selection.graphicType, "MA5"); } }); test("an untrusted s8010 entry blocks synchronization instead of being rewritten", () => { const untrusted = { id: "unknown-candle", builderKey: "s8010", code: "8051", enabled: true, fadeDuration: 6, selection: { graphicType: "" }, operator: { source: "unknown" } }; const result = workflow.synchronizePlaylist([untrusted], true, true, adapters); assert.equal(result.valid, false); assert.equal(result.changed, false); assert.deepEqual(result.invalidIds, ["unknown-candle"]); assert.equal(result.playlist[0], untrusted); }); test("a second synchronization with identical flags is stable", () => { const first = workflow.synchronizePlaylist(entries(), true, true, adapters); const second = workflow.synchronizePlaylist([...first.playlist], true, true, adapters); assert.equal(first.valid, true); assert.equal(second.valid, true); assert.equal(second.changed, false); second.playlist.forEach((entry, index) => assert.equal(entry, first.playlist[index])); });