269 lines
13 KiB
JavaScript
269 lines
13 KiB
JavaScript
"use strict";
|
|
|
|
const test = require("node:test");
|
|
const assert = require("node:assert/strict");
|
|
const workflow = require("../../Web/comparison-workflow.js");
|
|
|
|
const kospi = workflow.getMarketTarget("Kospi");
|
|
const kospi200 = workflow.getMarketTarget("코스피200 지수");
|
|
const futures = workflow.getMarketTarget("futures");
|
|
const wonDollar = workflow.getMarketTarget("WonDollar");
|
|
const samsung = Object.freeze({
|
|
kind: "krx-stock", market: "kospi", stockName: "삼성전자",
|
|
stockCode: "005930", displayName: "삼성전자"
|
|
});
|
|
const skHynix = Object.freeze({
|
|
kind: "krx-stock", market: "kospi", stockName: "SK하이닉스",
|
|
stockCode: "000660", displayName: "SK하이닉스"
|
|
});
|
|
const nxtSamsung = Object.freeze({
|
|
kind: "nxt-stock", market: "kospi", stockName: "삼성전자",
|
|
stockCode: "005930", displayName: "삼성전자(NXT)"
|
|
});
|
|
const apple = Object.freeze({
|
|
kind: "world-stock", inputName: "Apple Inc", displayName: "애플",
|
|
symbol: "NAS@AAPL", nation: "US"
|
|
});
|
|
|
|
test("comparison inventory pins the exact runtime asset, 24 targets and nine actions", () => {
|
|
assert.deepEqual(workflow.runtimeAsset, {
|
|
relativePath: "bin/Debug/Res/종목비교.ini",
|
|
sha256: "980F74C000EF6A34B85FDAAF6D2756269483637186C91A93548F35C5CFC1B7E4"
|
|
});
|
|
assert.equal(workflow.marketTargets.length, 24);
|
|
assert.equal(new Set(workflow.marketTargets.map(value => value.target)).size, 24);
|
|
assert.deepEqual(workflow.marketTargets.map(value => [value.displayName, value.target]), [
|
|
["코스피 지수", "Kospi"], ["코스닥 지수", "Kosdaq"],
|
|
["코스피200 지수", "Kospi200"], ["선물 지수", "Futures"],
|
|
["KRX100지수", "Krx100"], ["환율-원달러", "WonDollar"],
|
|
["환율-원엔", "WonYen"], ["환율-원위엔", "WonYuan"],
|
|
["환율-원유로", "WonEuro"], ["해외지수-다우", "Dow"],
|
|
["해외지수-나스닥", "Nasdaq"], ["해외지수-S&P", "Sp500"],
|
|
["해외지수-독일", "GermanyDax"], ["해외지수-영국", "UnitedKingdomFtse"],
|
|
["해외지수-프랑스", "FranceCac"], ["해외지수-일본", "Nikkei"],
|
|
["해외지수-홍콩", "HangSeng"], ["해외지수-대만", "TaiwanWeighted"],
|
|
["해외지수-싱가포르", "SingaporeStraitsTimes"], ["해외지수-태국", "ThailandSet"],
|
|
["해외지수-필리핀", "PhilippinesComposite"], ["해외지수-말레이시아", "MalaysiaKlse"],
|
|
["해외지수-인도네시아", "IndonesiaComposite"], ["해외지수-중국", "ShanghaiComposite"]
|
|
]);
|
|
assert.deepEqual(workflow.actions.map(value => value.label), [
|
|
"2열판 예상체결", "2열판", "종목별 비교분석_캔들 그래프", "종목별 비교분석_라인 그래프",
|
|
"5일", "1개월", "3개월", "6개월", "12개월"
|
|
]);
|
|
assert.deepEqual(workflow.yieldActionIds, [
|
|
"return-5d", "return-1m", "return-3m", "return-6m", "return-12m"
|
|
]);
|
|
});
|
|
|
|
test("typed market, KRX, NXT and world targets normalize without heuristic aliases", () => {
|
|
assert.equal(workflow.getMarketTarget("코스피200 지수"), kospi200);
|
|
assert.equal(workflow.getMarketTarget("sp500").target, "Sp500");
|
|
assert.deepEqual(workflow.normalizeTarget({
|
|
kind: "krx-stock", market: "KOSPI", name: " 삼성전자 ", code: "005930"
|
|
}), samsung);
|
|
assert.deepEqual(workflow.normalizeTarget({
|
|
kind: "nxt-stock", market: "KOSPI", name: "삼성전자", code: "005930"
|
|
}), nxtSamsung);
|
|
assert.deepEqual(workflow.normalizeTarget(apple), apple);
|
|
assert.equal(workflow.subjectTokenForTarget(kospi200), "Kospi200");
|
|
assert.equal(workflow.subjectTokenForTarget(nxtSamsung), "삼성전자(NXT)");
|
|
assert.equal(workflow.subjectTokenForTarget(apple), "Apple Inc");
|
|
assert.equal(workflow.targetKey(nxtSamsung), "nxt:kospi:005930");
|
|
|
|
assert.equal(workflow.normalizeTarget({ ...kospi, displayName: "코스피" }), null);
|
|
assert.equal(workflow.normalizeTarget({ ...samsung, stockName: "삼성,전자" }), null);
|
|
assert.equal(workflow.normalizeTarget({ ...nxtSamsung, displayName: "삼성전자" }), null);
|
|
assert.equal(workflow.normalizeTarget({ ...apple, nation: "JP" }), null);
|
|
assert.equal(workflow.normalizeTarget({ ...apple, symbol: "AAPL;DROP" }), null);
|
|
});
|
|
|
|
test("target double-click rotation, swap and clear preserve the original slot rules", () => {
|
|
const empty = workflow.clearPair();
|
|
assert.deepEqual(empty, [null, null]);
|
|
const first = workflow.rotatePair(empty, samsung);
|
|
assert.deepEqual(first, [samsung, null]);
|
|
const second = workflow.rotatePair(first, skHynix);
|
|
assert.deepEqual(second, [skHynix, samsung]);
|
|
assert.deepEqual(workflow.swapPair(second), [samsung, skHynix]);
|
|
assert.deepEqual(workflow.rotatePair(second, skHynix), [skHynix, skHynix]);
|
|
assert.equal(workflow.normalizePair(first), null);
|
|
});
|
|
|
|
test("CURRENT compatibility mirrors supported s8018 and s5032 branches and fails closed", () => {
|
|
assert.equal(workflow.isActionAllowed("two-column-current", [kospi, kospi200]), true);
|
|
assert.equal(workflow.isActionAllowed("two-column-current", [kospi, samsung]), true);
|
|
assert.equal(workflow.isActionAllowed("two-column-current", [kospi, nxtSamsung]), true);
|
|
assert.equal(workflow.isActionAllowed("two-column-current", [samsung, nxtSamsung]), true);
|
|
assert.equal(workflow.isActionAllowed("two-column-current", [apple, samsung]), true);
|
|
assert.equal(workflow.isActionAllowed("two-column-current", [apple, nxtSamsung]), true);
|
|
|
|
assert.equal(workflow.getCompatibility("two-column-current", [kospi, apple]).reason,
|
|
"mixed-market-and-world-is-unsupported");
|
|
assert.equal(workflow.getCompatibility("two-column-current", [futures, samsung]).reason,
|
|
"futures-requires-a-market-target-counterpart");
|
|
assert.equal(workflow.getCompatibility("two-column-current", [futures, futures]).reason,
|
|
"two-futures-targets-are-unsupported");
|
|
assert.equal(workflow.isActionAllowed("two-column-current", [futures, kospi]), true);
|
|
});
|
|
|
|
test("EXPECTED and graph compatibility expose only combinations the Core can resolve", () => {
|
|
assert.equal(workflow.isActionAllowed("two-column-expected", [kospi, samsung]), true);
|
|
assert.equal(workflow.isActionAllowed("two-column-expected", [samsung, skHynix]), true);
|
|
assert.equal(workflow.isActionAllowed("two-column-expected", [wonDollar, samsung]), false);
|
|
assert.equal(workflow.isActionAllowed("two-column-expected", [nxtSamsung, samsung]), false);
|
|
assert.equal(workflow.isActionAllowed("two-column-expected", [apple, samsung]), false);
|
|
|
|
const futuresExpected = workflow.getCompatibility("two-column-expected", [futures, wonDollar]);
|
|
assert.equal(futuresExpected.supported, true);
|
|
assert.equal(futuresExpected.effectiveMode, "CURRENT");
|
|
assert.match(futuresExpected.warning, /current s5032/i);
|
|
|
|
for (const actionId of ["comparison-candle", "comparison-line", ...workflow.yieldActionIds]) {
|
|
assert.equal(workflow.isActionAllowed(actionId, [samsung, skHynix]), true);
|
|
assert.equal(workflow.isActionAllowed(actionId, [samsung, nxtSamsung]), false);
|
|
assert.equal(workflow.isActionAllowed(actionId, [samsung, apple]), false);
|
|
assert.equal(workflow.isActionAllowed(actionId, [kospi, samsung]), false);
|
|
}
|
|
});
|
|
|
|
test("two-column builders use canonical market subjects and the correct dynamic alias", () => {
|
|
const current = workflow.createComparisonPlaylistEntry("two-column-current", [kospi200, samsung], {
|
|
id: "current-pair"
|
|
});
|
|
assert.equal(current.builderKey, "s8018");
|
|
assert.equal(current.code, "8032");
|
|
assert.deepEqual(current.aliases, ["8032"]);
|
|
assert.deepEqual(current.selection, {
|
|
groupCode: "INDEX", subject: "Kospi200,삼성전자",
|
|
graphicType: "2열판", subtype: "CURRENT", dataCode: ""
|
|
});
|
|
|
|
const stockPair = workflow.buildComparisonSelection("two-column-current", [nxtSamsung, apple]);
|
|
assert.equal(stockPair.builderKey, "s8018");
|
|
assert.equal(stockPair.selection.groupCode, "STOCK");
|
|
assert.equal(stockPair.selection.subject, "삼성전자(NXT),Apple Inc");
|
|
|
|
const futuresPair = workflow.createComparisonPlaylistEntry("two-column-expected", [wonDollar, futures], {
|
|
id: "futures-pair", fadeDuration: 0
|
|
});
|
|
assert.equal(futuresPair.builderKey, "s5032");
|
|
assert.equal(futuresPair.code, "5032");
|
|
assert.equal(futuresPair.selection.subtype, "EXPECTED");
|
|
assert.equal(futuresPair.effectiveMode, "CURRENT");
|
|
assert.equal(futuresPair.fadeDuration, 0);
|
|
});
|
|
|
|
test("candle, line and all five return actions create explicit selections in legacy order", () => {
|
|
const candle = workflow.createComparisonPlaylistEntry("comparison-candle", [samsung, skHynix], {
|
|
id: "candle"
|
|
});
|
|
assert.equal(candle.builderKey, "s5026");
|
|
assert.equal(candle.code, "5026");
|
|
assert.deepEqual(candle.selection, {
|
|
groupCode: "STOCK", subject: "삼성전자,SK하이닉스",
|
|
graphicType: "COMPARISON_CANDLE", subtype: "FiveDays",
|
|
dataCode: "KOSPI:005930|KOSPI:000660"
|
|
});
|
|
|
|
const line = workflow.createComparisonPlaylistEntry("comparison-line", [samsung, skHynix], {
|
|
id: "line"
|
|
});
|
|
assert.equal(line.builderKey, "s5087");
|
|
assert.equal(line.selection.graphicType, "COMPARISON_LINE");
|
|
assert.equal(line.selection.dataCode, "KOSPI:005930|KOSPI:000660");
|
|
|
|
const batch = workflow.createYieldBatchEntries([samsung, skHynix], {
|
|
idPrefix: "pair-yield", fadeDuration: 4
|
|
});
|
|
assert.equal(batch.length, 5);
|
|
assert.deepEqual(batch.map(value => value.id), [
|
|
"pair-yield-5d", "pair-yield-1m", "pair-yield-3m", "pair-yield-6m", "pair-yield-12m"
|
|
]);
|
|
assert.deepEqual(batch.map(value => value.selection.subtype), [
|
|
"FiveDays", "OneMonth", "ThreeMonths", "SixMonths", "TwelveMonths"
|
|
]);
|
|
assert.ok(batch.every(value => value.builderKey === "s5029" && value.code === "5029"));
|
|
});
|
|
|
|
test("trusted restore reconstructs selections and rejects stored-entry tampering", () => {
|
|
const original = workflow.createComparisonPlaylistEntry("two-column-current", [kospi, nxtSamsung], {
|
|
id: "stored", fadeDuration: 7
|
|
});
|
|
assert.ok(workflow.restoreComparisonPlaylistEntry(original));
|
|
assert.equal(workflow.restoreComparisonPlaylistEntry({ ...original, enabled: false }).enabled, false);
|
|
assert.equal(workflow.restoreComparisonPlaylistEntry({ ...original, code: "5032" }), null);
|
|
assert.equal(workflow.restoreComparisonPlaylistEntry({
|
|
...original, selection: { ...original.selection, subject: "Kospi,삼성전자" }
|
|
}), null);
|
|
assert.equal(workflow.restoreComparisonPlaylistEntry({
|
|
...original, operator: { ...original.operator, assetSha256: "0".repeat(64) }
|
|
}), null);
|
|
assert.equal(workflow.restoreComparisonPlaylistEntry({
|
|
...original, operator: { ...original.operator, schemaVersion: 2 }
|
|
}), null);
|
|
assert.equal(workflow.restoreComparisonPlaylistEntry({
|
|
...original,
|
|
operator: {
|
|
...original.operator,
|
|
pair: [{ ...kospi, displayName: "변조" }, nxtSamsung]
|
|
}
|
|
}), null);
|
|
});
|
|
|
|
test("saved pair list normalization is schema-versioned, bounded and duplicate-safe", () => {
|
|
const first = workflow.createPairRecord("first", [samsung, skHynix]);
|
|
const second = workflow.createPairRecord("second", [kospi, nxtSamsung]);
|
|
assert.ok(first);
|
|
assert.equal(workflow.createPairRecord("bad id", [samsung, skHynix]), null);
|
|
assert.deepEqual(workflow.normalizePairList({ schemaVersion: 2, pairs: [first] }), {
|
|
schemaVersion: 1, pairs: []
|
|
});
|
|
|
|
const normalized = workflow.normalizePairList({
|
|
schemaVersion: 1,
|
|
pairs: [first, { id: "invalid", pair: [samsung, null] }, first, second]
|
|
});
|
|
assert.deepEqual(normalized.pairs.map(value => value.id), ["first", "second"]);
|
|
assert.ok(Object.isFrozen(normalized));
|
|
assert.ok(Object.isFrozen(normalized.pairs));
|
|
|
|
const appended = workflow.appendPair(normalized, "third", [apple, nxtSamsung]);
|
|
assert.deepEqual(appended.pairs.map(value => value.id), ["first", "second", "third"]);
|
|
assert.deepEqual(normalized.pairs.map(value => value.id), ["first", "second"]);
|
|
assert.throws(() => workflow.appendPair(appended, first), /already exists/i);
|
|
});
|
|
|
|
test("saved pair rows reorder and delete persistently even at the empty-list boundary", () => {
|
|
let list = workflow.deleteAllPairs();
|
|
list = workflow.appendPair(list, "first", [samsung, skHynix]);
|
|
list = workflow.appendPair(list, "second", [kospi, nxtSamsung]);
|
|
list = workflow.appendPair(list, "third", [apple, samsung]);
|
|
|
|
assert.deepEqual(workflow.reorderPairList(list, "first", "up").pairs.map(value => value.id),
|
|
["first", "second", "third"]);
|
|
list = workflow.reorderPairList(list, "third", -1);
|
|
assert.deepEqual(list.pairs.map(value => value.id), ["first", "third", "second"]);
|
|
list = workflow.reorderPairList(list, "first", "down");
|
|
assert.deepEqual(list.pairs.map(value => value.id), ["third", "first", "second"]);
|
|
list = workflow.deletePair(list, "first");
|
|
assert.deepEqual(list.pairs.map(value => value.id), ["third", "second"]);
|
|
list = workflow.deleteAllPairs();
|
|
assert.deepEqual(list, { schemaVersion: 1, pairs: [] });
|
|
});
|
|
|
|
test("unsafe ids, fades, incomplete pairs and unsupported combinations cannot create entries", () => {
|
|
assert.throws(() => workflow.createComparisonPlaylistEntry("two-column-current", [samsung, null], {
|
|
id: "missing"
|
|
}), /two comparison targets/i);
|
|
assert.throws(() => workflow.createComparisonPlaylistEntry("two-column-current", [kospi, apple], {
|
|
id: "unsupported"
|
|
}), /mixed-market-and-world/i);
|
|
assert.throws(() => workflow.createComparisonPlaylistEntry("two-column-current", [samsung, skHynix], {
|
|
id: "bad id"
|
|
}), /safe comparison playlist id/i);
|
|
assert.throws(() => workflow.createComparisonPlaylistEntry("two-column-current", [samsung, skHynix], {
|
|
id: "bad-fade", fadeDuration: 61
|
|
}), /0 through 60/i);
|
|
assert.equal(workflow.getCompatibility("unknown", [samsung, skHynix]).reason, "unknown-action");
|
|
});
|