"use strict"; const test = require("node:test"); const assert = require("node:assert/strict"); const workflow = require("../../Web/industry-workflow.js"); const kospi = Object.freeze({ market: "kospi", name: "전기전자", code: "013" }); const kospiSecond = Object.freeze({ market: "kospi", name: "운수장비", code: "015" }); const kosdaq = Object.freeze({ market: "kosdaq", name: "반도체", code: "027" }); const kosdaqSecond = Object.freeze({ market: "kosdaq", name: "IT부품", code: "029" }); test("industry runtime assets expose 22 exact cut templates per market", () => { assert.equal(workflow.cuts.length, 22); assert.equal(new Set(workflow.cuts.map(value => value.id)).size, 22); assert.equal(workflow.runtimeAssets.kospi.sha256, "F21DE58003315EAB41F9FE7B5A573C71653056203E6743D05AA96E7FF1B8BF52"); assert.equal(workflow.runtimeAssets.kosdaq.sha256, "F25B8926E8F20FC5FBC94A1891A9E2EAD22852E8C829321EF1A001FE3F42ECE4"); }); test("native industry rows normalize only bounded KOSPI and KOSDAQ identities", () => { assert.deepEqual(workflow.normalizeIndustry({ market: "KOSPI", name: " 전기전자 ", code: "013" }), kospi); assert.equal(workflow.normalizeIndustry({ market: "nxt-kospi", name: "전기전자", code: "013" }), null); assert.equal(workflow.normalizeIndustry({ market: "kospi", name: "전기\n전자", code: "013" }), null); assert.equal(workflow.normalizeIndustry({ market: "kospi", name: "전기전자", code: "01-3" }), null); }); test("single, pair, yield and candle actions project closed legacy selections", () => { const single = workflow.createIndustryPlaylistEntry("kospi", "one-column", { id: "single", selected: kospi }); assert.equal(single.builderKey, "s5001"); assert.equal(single.code, "5001"); assert.deepEqual(single.selection, { groupCode: "INDUSTRY_KOSPI", subject: "전기전자", graphicType: "1열판", subtype: "CURRENT", dataCode: "" }); const pair = workflow.createIndustryPlaylistEntry("kosdaq", "two-column", { id: "pair", pair: [kosdaq, kosdaqSecond] }); assert.equal(pair.builderKey, "s8018"); assert.equal(pair.code, "8032"); assert.equal(pair.selection.subject, "반도체-IT부품"); const yieldEntry = workflow.createIndustryPlaylistEntry("kospi", "yield-120일", { id: "yield", selected: kospi }); assert.equal(yieldEntry.builderKey, "s5086"); assert.equal(yieldEntry.selection.subtype, "OneHundredTwentyDays"); const candle = workflow.createIndustryPlaylistEntry("kosdaq", "candle-volume-240일", { id: "candle", selected: kosdaq }); assert.equal(candle.builderKey, "s8010"); assert.equal(candle.code, "8056"); assert.equal(candle.selection.groupCode, "KOSDAQ_INDUSTRY"); assert.equal(candle.selection.subtype, "VOLUME"); }); test("market-wide industry actions need no selected industry and use explicit aliases", () => { const now = new Date(2026, 6, 11, 10, 0, 0); const five = workflow.createIndustryPlaylistEntry("kosdaq", "five-row", { id: "five", now }); assert.equal(five.builderKey, "s5074"); assert.equal(five.selection.groupCode, "PAGED_DOMESTIC_KOSDAQ"); assert.equal(five.selection.subtype, "INDUSTRY"); assert.equal(five.selection.dataCode, "2026-07-11"); const square = workflow.createIndustryPlaylistEntry("kosdaq", "square", { id: "square" }); assert.equal(square.builderKey, "s8001"); assert.equal(square.code, "8002"); assert.equal(square.selection.subject, "KOSDAQ"); const sector = workflow.createIndustryPlaylistEntry("kospi", "sector", { id: "sector" }); assert.equal(sector.builderKey, "s5078"); assert.equal(sector.selection.graphicType, "SECTOR_INDEX"); }); test("industry candles carry and restore the global moving-average flags", () => { const value = workflow.createIndustryPlaylistEntry("kospi", "candle-120일", { id: "industry-candle-flags", selected: kospi, ma5: true, ma20: true }); assert.equal(value.selection.graphicType, "MA5,MA20"); assert.deepEqual(value.operator.movingAverages, { ma5: true, ma20: true }); assert.deepEqual(workflow.restoreIndustryPlaylistEntry(value), value); assert.equal(workflow.restoreIndustryPlaylistEntry({ ...value, selection: { ...value.selection, graphicType: "MA20" } }), null); }); test("legacy industry candle entries gain closed moving-average metadata on restore", () => { const value = workflow.createIndustryPlaylistEntry("kosdaq", "candle-volume-20일", { id: "industry-candle-legacy", selected: kosdaq }); const legacy = { ...value, operator: Object.freeze(Object.fromEntries( Object.entries(value.operator).filter(([key]) => key !== "movingAverages"))) }; assert.deepEqual( workflow.restoreIndustryPlaylistEntry(legacy).operator.movingAverages, { ma5: false, ma20: false }); }); test("selection prerequisites and cross-market values fail closed", () => { assert.equal(workflow.isCutAllowed("kospi", "one-column", null, null, null), false); assert.equal(workflow.isCutAllowed("kospi", "two-column", kospi, kospi, null), false); assert.equal(workflow.isCutAllowed("kospi", "two-column", null, kospi, kospiSecond), true); assert.equal(workflow.isCutAllowed("kospi", "five-row", null, null, null), true); assert.throws(() => workflow.createIndustryPlaylistEntry("kospi", "one-column", { id: "cross", selected: kosdaq })); }); test("stored industry entries restore from trusted bindings and reject tampering", () => { const value = workflow.createIndustryPlaylistEntry("kospi", "two-column", { id: "stored-pair", pair: [kospi, kospiSecond] }); assert.ok(workflow.restoreIndustryPlaylistEntry(value)); assert.equal(workflow.restoreIndustryPlaylistEntry({ ...value, code: "5032" }), null); assert.equal(workflow.restoreIndustryPlaylistEntry({ ...value, selection: { ...value.selection, subject: "전기전자-화학" } }), null); assert.equal(workflow.restoreIndustryPlaylistEntry({ ...value, operator: { ...value.operator, assetSha256: "0".repeat(64) } }), null); }); test("paged industry dates refresh at restore time", () => { const original = workflow.createIndustryPlaylistEntry("kospi", "five-row", { id: "paged", now: new Date(2026, 6, 10, 23, 59, 0) }); const refreshed = workflow.restoreIndustryPlaylistEntry(original, { now: new Date(2026, 6, 11, 0, 1, 0) }); assert.equal(refreshed.selection.dataCode, "2026-07-11"); });