Complete legacy operator UI and playout migration

This commit is contained in:
2026-07-12 02:05:49 +09:00
parent d2e16bf0c2
commit ffb8f43c19
131 changed files with 48473 additions and 228 deletions

View File

@@ -0,0 +1,142 @@
"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");
});