Migrate remaining legacy operator workflows
This commit is contained in:
446
tests/Web/legacy-named-row-workflow.test.cjs
Normal file
446
tests/Web/legacy-named-row-workflow.test.cjs
Normal file
@@ -0,0 +1,446 @@
|
||||
"use strict";
|
||||
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const legacyRows = require("../../Web/legacy-named-row-workflow.js");
|
||||
const namedPlaylists = require("../../Web/named-playlist-workflow.js");
|
||||
const stocks = require("../../Web/operator-workflow.js");
|
||||
const fixed = require("../../Web/legacy-fixed-workflow.js");
|
||||
const themes = require("../../Web/theme-workflow.js");
|
||||
const experts = require("../../Web/expert-workflow.js");
|
||||
const comparisons = require("../../Web/comparison-workflow.js");
|
||||
const industries = require("../../Web/industry-workflow.js");
|
||||
|
||||
function rawRow(overrides = {}) {
|
||||
return {
|
||||
itemIndex: 0,
|
||||
enabled: true,
|
||||
groupCode: "코스피",
|
||||
subject: "삼성전자",
|
||||
graphicType: "1열판기본",
|
||||
subtype: "현재가",
|
||||
pageText: "1/1",
|
||||
dataCode: "005930",
|
||||
...overrides
|
||||
};
|
||||
}
|
||||
|
||||
function legacySelection(value) {
|
||||
const { groupCode, subject, graphicType, subtype, dataCode } = value;
|
||||
return { groupCode, subject, graphicType, subtype, dataCode };
|
||||
}
|
||||
|
||||
test("the original MainForm Korean stock row maps to exactly one canonical stock cut", () => {
|
||||
const entry = stocks.createStockPlaylistEntry({
|
||||
market: "kospi",
|
||||
stockName: "삼성전자",
|
||||
displayName: "삼성전자",
|
||||
stockCode: "005930",
|
||||
isNxt: false
|
||||
}, "basic-current", { id: "stock-1", fadeDuration: 6 });
|
||||
const raw = rawRow();
|
||||
|
||||
assert.equal(legacyRows.matchesCanonicalEntry(entry, raw), true);
|
||||
assert.deepEqual(legacyRows.legacySelectionForEntry(entry), legacySelection(raw));
|
||||
assert.equal(legacyRows.selectUniqueCanonicalEntry([entry], raw), entry);
|
||||
for (const [field, value] of [
|
||||
["groupCode", "코스닥"],
|
||||
["subject", "삼성전자우"],
|
||||
["graphicType", "1열판상세"],
|
||||
["subtype", "예상체결가"],
|
||||
["dataCode", "000660"]
|
||||
]) {
|
||||
assert.equal(legacyRows.matchesCanonicalEntry(entry, { ...raw, [field]: value }), false, field);
|
||||
}
|
||||
});
|
||||
|
||||
test("all 322 closed fixed actions reproduce their original UC1 Korean row signature", () => {
|
||||
const available = fixed.actions.filter(action => action.available);
|
||||
assert.equal(available.length, 322);
|
||||
const signatures = new Set();
|
||||
|
||||
for (const action of available) {
|
||||
const signature = legacyRows.fixedLegacySignature(action);
|
||||
assert.ok(signature, action.id);
|
||||
assert.equal(legacyRows.matchesFixedActionRaw(signature, action), true, action.id);
|
||||
const entry = fixed.createFixedPlaylistEntry(action.id, {
|
||||
id: `entry-${action.id}`,
|
||||
fadeDuration: 6,
|
||||
now: new Date(2026, 6, 12, 9, 0, 0)
|
||||
});
|
||||
assert.equal(legacyRows.matchesCanonicalEntry(entry, signature), true, action.id);
|
||||
assert.deepEqual(legacyRows.legacySelectionForEntry(entry), signature, action.id);
|
||||
signatures.add(JSON.stringify(signature));
|
||||
}
|
||||
|
||||
assert.equal(signatures.size, 322, "legacy fixed rows must remain unambiguous");
|
||||
const dow = fixed.actions.find(action => action.id === "fixed-001");
|
||||
assert.deepEqual(legacyRows.fixedLegacySignature(dow), {
|
||||
groupCode: "해외지수",
|
||||
subject: "다우",
|
||||
graphicType: "1열판기본",
|
||||
subtype: "",
|
||||
dataCode: ""
|
||||
});
|
||||
assert.equal(legacyRows.matchesFixedActionRaw({
|
||||
...legacyRows.fixedLegacySignature(dow), subtype: "현재가"
|
||||
}, dow), false);
|
||||
});
|
||||
|
||||
test("the original UC4 theme rows map only through the closed action and sort grammar", () => {
|
||||
const raw = rawRow({
|
||||
groupCode: "테마",
|
||||
subject: "반도체",
|
||||
graphicType: "5단 표그래프",
|
||||
subtype: "테마-현재가(상승률순)",
|
||||
pageText: "1/4",
|
||||
dataCode: "0123"
|
||||
});
|
||||
const descriptor = legacyRows.themeLegacyDescriptor(raw);
|
||||
assert.deepEqual(descriptor, {
|
||||
actionId: "five-row-current",
|
||||
sort: "GAIN_DESC",
|
||||
theme: {
|
||||
market: "krx",
|
||||
themeCode: "0123",
|
||||
title: "반도체",
|
||||
session: null,
|
||||
itemCount: 1
|
||||
}
|
||||
});
|
||||
const entry = themes.createThemePlaylistEntry(
|
||||
descriptor.actionId,
|
||||
descriptor.theme,
|
||||
descriptor.sort,
|
||||
{ id: "theme-1", fadeDuration: 6 });
|
||||
assert.equal(legacyRows.matchesCanonicalEntry(entry, raw), true);
|
||||
assert.deepEqual(legacyRows.legacySelectionForEntry(entry), legacySelection(raw));
|
||||
const originalFallback = { ...raw, subtype: "테마-현재가" };
|
||||
const fallbackDescriptor = legacyRows.themeLegacyDescriptor(originalFallback);
|
||||
assert.equal(fallbackDescriptor.sort, "GAIN_ASC");
|
||||
const fallbackEntry = themes.createThemePlaylistEntry(
|
||||
fallbackDescriptor.actionId,
|
||||
fallbackDescriptor.theme,
|
||||
fallbackDescriptor.sort,
|
||||
{ id: "theme-fallback", fadeDuration: 6 });
|
||||
assert.equal(legacyRows.matchesCanonicalEntry(fallbackEntry, originalFallback), true);
|
||||
assert.equal(
|
||||
legacyRows.legacySelectionForEntry(fallbackEntry).subtype,
|
||||
"테마-현재가(하락률순)",
|
||||
"new saves make the original implicit fallback explicit");
|
||||
assert.equal(legacyRows.themeLegacyDescriptor({
|
||||
...raw, subtype: "테마-현재가(상승률)"
|
||||
}), null);
|
||||
const nxtRaw = {
|
||||
...raw,
|
||||
groupCode: "테마_NXT",
|
||||
subject: "반도체(NXT)",
|
||||
subtype: "테마-현재가(입력순)"
|
||||
};
|
||||
const morning = legacyRows.themeLegacyDescriptor(nxtRaw, {
|
||||
now: new Date(2026, 6, 12, 12, 59, 59)
|
||||
});
|
||||
const afternoon = legacyRows.themeLegacyDescriptor(nxtRaw, {
|
||||
now: new Date(2026, 6, 12, 13, 0, 0)
|
||||
});
|
||||
assert.equal(morning.theme.session, "PRE_MARKET");
|
||||
assert.equal(afternoon.theme.session, "AFTER_MARKET");
|
||||
const nxtEntry = themes.createThemePlaylistEntry(
|
||||
morning.actionId,
|
||||
morning.theme,
|
||||
morning.sort,
|
||||
{ id: "theme-nxt-legacy", fadeDuration: 6 });
|
||||
assert.equal(legacyRows.matchesCanonicalEntry(nxtEntry, nxtRaw, {
|
||||
now: new Date(2026, 6, 12, 12, 30, 0)
|
||||
}), true);
|
||||
assert.equal(legacyRows.matchesCanonicalEntry(nxtEntry, nxtRaw, {
|
||||
now: new Date(2026, 6, 12, 13, 0, 0)
|
||||
}), false);
|
||||
assert.equal(legacyRows.legacySelectionForEntry(nxtEntry), null,
|
||||
"new named saves remain blocked because the old row does not persist a session");
|
||||
assert.equal(legacyRows.matchesCanonicalEntry(entry, { ...raw, dataCode: "87654321" }), false);
|
||||
});
|
||||
|
||||
test("the original UC6 expert row maps to the canonical paged expert entry", () => {
|
||||
const raw = rawRow({
|
||||
groupCode: "전문가 추천",
|
||||
subject: "김전문",
|
||||
graphicType: "5단 표그래프",
|
||||
subtype: "현재가",
|
||||
pageText: "1/2",
|
||||
dataCode: "0123"
|
||||
});
|
||||
const descriptor = legacyRows.expertLegacyDescriptor(raw);
|
||||
assert.deepEqual(descriptor, {
|
||||
expertCode: "0123",
|
||||
expertName: "김전문",
|
||||
source: "oracle",
|
||||
itemCount: 1,
|
||||
previewTruncated: false
|
||||
});
|
||||
const entry = experts.createExpertPlaylistEntry(descriptor, {
|
||||
id: "expert-1",
|
||||
fadeDuration: 6
|
||||
});
|
||||
assert.equal(legacyRows.matchesCanonicalEntry(entry, raw), true);
|
||||
assert.deepEqual(legacyRows.legacySelectionForEntry(entry), legacySelection(raw));
|
||||
assert.equal(legacyRows.expertLegacyDescriptor({ ...raw, dataCode: "123" }), null);
|
||||
assert.equal(legacyRows.matchesCanonicalEntry(entry, { ...raw, subject: "다른전문가" }), false);
|
||||
});
|
||||
|
||||
test("comparison rows project only identities that the original seven fields preserve", () => {
|
||||
const fixedEntry = comparisons.createComparisonPlaylistEntry("two-column-current", [
|
||||
comparisons.getMarketTarget("Kospi"),
|
||||
comparisons.getMarketTarget("Kosdaq")
|
||||
], { id: "comparison-fixed", fadeDuration: 6 });
|
||||
const fixedRaw = rawRow({
|
||||
groupCode: "지수",
|
||||
subject: "코스피 지수,코스닥 지수",
|
||||
graphicType: "2열판",
|
||||
subtype: "",
|
||||
dataCode: ""
|
||||
});
|
||||
assert.deepEqual(legacyRows.comparisonLegacySignature(fixedEntry.operator), legacySelection(fixedRaw));
|
||||
assert.equal(legacyRows.matchesCanonicalEntry(fixedEntry, fixedRaw), true);
|
||||
assert.deepEqual(legacyRows.legacySelectionForEntry(fixedEntry), legacySelection(fixedRaw));
|
||||
const exchangeOnly = comparisons.createComparisonPlaylistEntry("two-column-current", [
|
||||
comparisons.getMarketTarget("WonDollar"),
|
||||
comparisons.getMarketTarget("WonYen")
|
||||
], { id: "comparison-exchange-lossy", fadeDuration: 6 });
|
||||
assert.equal(legacyRows.legacySelectionForEntry(exchangeOnly), null);
|
||||
|
||||
const pair = [
|
||||
{ kind: "krx-stock", market: "kospi", stockName: "삼성전자", stockCode: "005930" },
|
||||
{ kind: "krx-stock", market: "kosdaq", stockName: "에이비엘바이오", stockCode: "298380" }
|
||||
];
|
||||
const candle = comparisons.createComparisonPlaylistEntry(
|
||||
"comparison-candle", pair, { id: "comparison-candle", fadeDuration: 6 });
|
||||
const candleRaw = rawRow({
|
||||
groupCode: "코스피,코스닥",
|
||||
subject: "삼성전자,에이비엘바이오",
|
||||
graphicType: "종목별 비교분석_캔들 그래프",
|
||||
subtype: "",
|
||||
dataCode: ""
|
||||
});
|
||||
assert.equal(legacyRows.matchesCanonicalEntry(candle, candleRaw), true);
|
||||
assert.deepEqual(legacyRows.legacySelectionForEntry(candle), legacySelection(candleRaw));
|
||||
|
||||
const oneMonth = comparisons.createComparisonPlaylistEntry(
|
||||
"return-1m", pair, { id: "comparison-return", fadeDuration: 6 });
|
||||
assert.deepEqual(legacyRows.legacySelectionForEntry(oneMonth), {
|
||||
groupCode: "코스피,코스닥",
|
||||
subject: "삼성전자,에이비엘바이오",
|
||||
graphicType: "종목별 수익률 비교",
|
||||
subtype: "1개월",
|
||||
dataCode: ""
|
||||
});
|
||||
|
||||
const lossyStockPlate = comparisons.createComparisonPlaylistEntry(
|
||||
"two-column-current", pair, { id: "comparison-lossy", fadeDuration: 6 });
|
||||
assert.equal(legacyRows.legacySelectionForEntry(lossyStockPlate), null);
|
||||
assert.equal(legacyRows.matchesCanonicalEntry(lossyStockPlate, {
|
||||
...fixedRaw,
|
||||
groupCode: "종목",
|
||||
subject: "삼성전자,에이비엘바이오"
|
||||
}), false);
|
||||
});
|
||||
|
||||
test("identity-free industry market rows map only to the three closed fixed actions", () => {
|
||||
const now = new Date(2026, 6, 12, 9, 0, 0);
|
||||
const cases = [
|
||||
["kospi", "five-row", "업종_코스피", "코스피", "5단 표그래프"],
|
||||
["kospi", "square", "업종_코스피", "코스피", "네모그래프"],
|
||||
["kospi", "sector", "업종_코스피", "코스피", "섹터지수"],
|
||||
["kosdaq", "five-row", "업종_코스닥", "코스닥", "5단 표그래프"],
|
||||
["kosdaq", "square", "업종_코스닥", "코스닥", "네모그래프"],
|
||||
["kosdaq", "sector", "업종_코스닥", "코스닥", "섹터지수"]
|
||||
];
|
||||
for (let index = 0; index < cases.length; index += 1) {
|
||||
const [market, cutId, groupCode, subject, graphicType] = cases[index];
|
||||
const raw = rawRow({ groupCode, subject, graphicType, subtype: "", dataCode: "" });
|
||||
const descriptor = legacyRows.industryFixedDescriptor(raw);
|
||||
assert.deepEqual(descriptor, { market, cutId });
|
||||
const entry = industries.createIndustryPlaylistEntry(market, cutId, {
|
||||
id: `industry-fixed-${index}`,
|
||||
fadeDuration: 6,
|
||||
now
|
||||
});
|
||||
assert.deepEqual(legacyRows.industryFixedLegacySignature(entry.operator), legacySelection(raw));
|
||||
assert.equal(legacyRows.matchesCanonicalEntry(entry, raw), true);
|
||||
assert.deepEqual(legacyRows.legacySelectionForEntry(entry), legacySelection(raw));
|
||||
if (cutId === "five-row") {
|
||||
assert.equal(entry.selection.dataCode, "2026-07-12");
|
||||
assert.equal(entry.builderKey, "s5074");
|
||||
}
|
||||
}
|
||||
|
||||
const selected = { market: "kospi", name: "전기전자", code: "013" };
|
||||
const individual = industries.createIndustryPlaylistEntry("kospi", "one-column", {
|
||||
id: "industry-identity-required",
|
||||
selected
|
||||
});
|
||||
assert.equal(legacyRows.industryFixedLegacySignature(individual.operator), null);
|
||||
assert.equal(legacyRows.industryFixedDescriptor({
|
||||
...rawRow(), groupCode: "업종_코스피", subject: "전기전자",
|
||||
graphicType: "1열판", subtype: "", dataCode: ""
|
||||
}), null);
|
||||
});
|
||||
|
||||
test("named restoration accepts a closed legacy mapping but rejects it by default or after tamper", () => {
|
||||
const raw = rawRow();
|
||||
const load = namedPlaylists.normalizeLoadResponse({
|
||||
requestId: "load-legacy-1",
|
||||
definition: { programCode: "00000001", title: "Legacy" },
|
||||
retrievedAt: "2026-07-12T09:00:00+09:00",
|
||||
totalRowCount: 1,
|
||||
canMutate: true,
|
||||
writeQuarantined: false,
|
||||
rows: [raw]
|
||||
});
|
||||
const entry = stocks.createStockPlaylistEntry({
|
||||
market: "kospi",
|
||||
stockName: "삼성전자",
|
||||
displayName: "삼성전자",
|
||||
stockCode: "005930",
|
||||
isNxt: false
|
||||
}, "basic-current", { id: "stock-restore-1", fadeDuration: 6 });
|
||||
|
||||
assert.equal(namedPlaylists.restoreRawRows(load, () => entry).rows[0].trusted, false);
|
||||
assert.equal(namedPlaylists.restoreRawRows(
|
||||
load, () => entry, legacyRows.matchesCanonicalEntry).rows[0].trusted, true);
|
||||
|
||||
const tamperedLoad = namedPlaylists.normalizeLoadResponse({
|
||||
...{
|
||||
requestId: "load-legacy-2",
|
||||
definition: { programCode: "00000001", title: "Legacy" },
|
||||
retrievedAt: "2026-07-12T09:00:00+09:00",
|
||||
totalRowCount: 1,
|
||||
canMutate: true,
|
||||
writeQuarantined: false
|
||||
},
|
||||
rows: [{ ...raw, dataCode: "000660" }]
|
||||
});
|
||||
assert.equal(namedPlaylists.restoreRawRows(
|
||||
tamperedLoad, () => entry, legacyRows.matchesCanonicalEntry).rows[0].trusted, false);
|
||||
assert.throws(() => namedPlaylists.restoreRawRows(load, () => entry, true), /resolver/i);
|
||||
});
|
||||
|
||||
test("canonical rows that identify multiple fixed actions remain blocked as ambiguous", () => {
|
||||
const first = fixed.createFixedPlaylistEntry("fixed-136", {
|
||||
id: "candle-1",
|
||||
fadeDuration: 6,
|
||||
now: new Date(2026, 6, 12, 9, 0, 0)
|
||||
});
|
||||
const second = fixed.createFixedPlaylistEntry("fixed-137", {
|
||||
id: "candle-2",
|
||||
fadeDuration: 6,
|
||||
now: new Date(2026, 6, 12, 9, 0, 0)
|
||||
});
|
||||
assert.deepEqual(first.selection, second.selection);
|
||||
const raw = legacySelection(first.selection);
|
||||
assert.equal(legacyRows.matchesCanonicalEntry(first, raw), true);
|
||||
assert.equal(legacyRows.matchesCanonicalEntry(second, raw), true);
|
||||
assert.equal(legacyRows.selectUniqueCanonicalEntry([first, second], raw), null);
|
||||
});
|
||||
|
||||
test("save projection is closed and refuses unknown or lossy NXT theme entries", () => {
|
||||
assert.equal(legacyRows.legacySelectionForEntry({
|
||||
selection: {
|
||||
groupCode: "FREE", subject: "text", graphicType: "type", subtype: "sub", dataCode: "code"
|
||||
},
|
||||
operator: { source: "unknown" }
|
||||
}), null);
|
||||
|
||||
const fixedEntry = fixed.createFixedPlaylistEntry("fixed-001", {
|
||||
id: "tampered-fixed-save",
|
||||
fadeDuration: 6,
|
||||
now: new Date(2026, 6, 12, 9, 0, 0)
|
||||
});
|
||||
assert.equal(legacyRows.legacySelectionForEntry({
|
||||
...fixedEntry,
|
||||
operator: { ...fixedEntry.operator, actionId: "fixed-002" }
|
||||
}), null);
|
||||
|
||||
const nxt = themes.createThemePlaylistEntry("five-row-current", {
|
||||
market: "nxt",
|
||||
themeCode: "0123",
|
||||
title: "NXT 테마",
|
||||
session: "PRE_MARKET",
|
||||
itemCount: 5
|
||||
}, "INPUT_ORDER", { id: "nxt-theme-save", fadeDuration: 6 });
|
||||
assert.equal(legacyRows.legacySelectionForEntry(nxt), null);
|
||||
assert.equal(legacyRows.legacySelectionForEntry({
|
||||
...nxt,
|
||||
selection: { ...nxt.selection, subject: "bad^value" }
|
||||
}), null);
|
||||
});
|
||||
|
||||
test("named replace serializes stock, fixed, theme and expert as original Korean rows", () => {
|
||||
const entries = [
|
||||
stocks.createStockPlaylistEntry({
|
||||
market: "kospi",
|
||||
stockName: "삼성전자",
|
||||
displayName: "삼성전자",
|
||||
stockCode: "005930",
|
||||
isNxt: false
|
||||
}, "basic-current", { id: "save-stock", fadeDuration: 6 }),
|
||||
fixed.createFixedPlaylistEntry("fixed-001", {
|
||||
id: "save-fixed",
|
||||
fadeDuration: 6,
|
||||
now: new Date(2026, 6, 12, 9, 0, 0)
|
||||
}),
|
||||
themes.createThemePlaylistEntry("five-row-current", {
|
||||
market: "krx",
|
||||
themeCode: "0123",
|
||||
title: "반도체",
|
||||
session: null,
|
||||
itemCount: 5
|
||||
}, "GAIN_DESC", { id: "save-theme", fadeDuration: 6 }),
|
||||
experts.createExpertPlaylistEntry({
|
||||
expertCode: "0123",
|
||||
expertName: "김전문",
|
||||
source: "oracle",
|
||||
itemCount: 5,
|
||||
previewTruncated: false
|
||||
}, { id: "save-expert", fadeDuration: 6 })
|
||||
];
|
||||
const request = namedPlaylists.createReplaceRequest(
|
||||
"replace-legacy-projection",
|
||||
"00000001",
|
||||
entries,
|
||||
{
|
||||
isTrustedEntry: entry => legacyRows.legacySelectionForEntry(entry) !== null,
|
||||
fieldsForEntry(entry, _index, pageText) {
|
||||
const selection = legacyRows.legacySelectionForEntry(entry);
|
||||
return [
|
||||
entry.enabled ? "1" : "0",
|
||||
selection.groupCode,
|
||||
selection.subject,
|
||||
selection.graphicType,
|
||||
selection.subtype,
|
||||
pageText,
|
||||
selection.dataCode
|
||||
];
|
||||
}
|
||||
});
|
||||
|
||||
assert.deepEqual(request.items, [
|
||||
{
|
||||
itemIndex: 0, enabled: true, groupCode: "코스피", subject: "삼성전자",
|
||||
graphicType: "1열판기본", subtype: "현재가", pageText: "1/1", dataCode: "005930"
|
||||
},
|
||||
{
|
||||
itemIndex: 1, enabled: true, groupCode: "해외지수", subject: "다우",
|
||||
graphicType: "1열판기본", subtype: "", pageText: "1/1", dataCode: ""
|
||||
},
|
||||
{
|
||||
itemIndex: 2, enabled: true, groupCode: "테마", subject: "반도체",
|
||||
graphicType: "5단 표그래프", subtype: "테마-현재가(상승률순)",
|
||||
pageText: "1/1", dataCode: "0123"
|
||||
},
|
||||
{
|
||||
itemIndex: 3, enabled: true, groupCode: "전문가 추천", subject: "김전문",
|
||||
graphicType: "5단 표그래프", subtype: "현재가", pageText: "1/1", dataCode: "0123"
|
||||
}
|
||||
]);
|
||||
});
|
||||
Reference in New Issue
Block a user