Files
MBN_STOCK_WEBVIEW/tests/Web/legacy-foreign-index-candle-workflow.test.cjs

243 lines
8.7 KiB
JavaScript

"use strict";
const test = require("node:test");
const assert = require("node:assert/strict");
const workflow = require("../../Web/legacy-foreign-index-candle-workflow.js");
const profiles = [
["다우존스", "Dow", "DJI@DJI"],
["나스닥", "Nasdaq", "NAS@IXIC"],
["S&P500", "Sp500", "SPI@SPX"]
];
const periods = [
["5일", 5, "8061"],
["20일", 20, "8040"],
["60일", 60, "8046"],
["120일", 120, "8051"],
["240일", 240, "8056"]
];
function raw(subject = "다우존스", subtype = "5일", itemIndex = 0, enabled = true) {
return {
itemIndex,
enabled,
groupCode: "해외지수",
subject,
graphicType: "캔들그래프",
subtype,
pageText: "1/1",
dataCode: ""
};
}
function outcome(pending) {
return {
itemIndex: pending.itemIndex,
status: "resolved",
targetKey: pending.targetKey,
inputName: pending.inputName,
symbol: pending.symbol,
nationCode: "US",
foreignDomesticCode: "0",
sceneCode: "s8010",
valueType: "PRICE",
periodDays: pending.periodDays,
cutCode: pending.cutCode
};
}
function response(requestId, pendingRows, rows = pendingRows.map(outcome)) {
return {
requestId,
retrievedAt: "2026-07-12T12:34:56+09:00",
totalRowCount: rows.length,
rows
};
}
test("all three fixed targets and five periods classify including inactive 240-day contract", () => {
let itemIndex = 0;
for (const [subject, targetKey, symbol] of profiles) {
for (const [legacyPeriod, periodDays, cutCode] of periods) {
const pending = workflow.classify(raw(subject, legacyPeriod, itemIndex), {
id: `foreign_${itemIndex}`,
fadeDuration: 6
});
assert.ok(pending);
assert.equal(pending.targetKey, targetKey);
assert.equal(pending.symbol, symbol);
assert.equal(pending.periodDays, periodDays);
assert.equal(pending.cutCode, cutCode);
assert.equal(workflow.isPending(pending), true);
itemIndex += 1;
}
}
assert.equal(itemIndex, 15);
});
test("raw classifier is exact and preserves disabled state", () => {
const disabled = workflow.classify(raw("다우존스", "240일", 9, false), {
id: "foreign_disabled",
fadeDuration: 0
});
assert.ok(disabled);
assert.equal(disabled.enabled, false);
for (const invalid of [
{ ...raw(), groupCode: "FOREIGN_INDEX" },
{ ...raw(), subject: "다우" },
{ ...raw(), graphicType: "캔들 그래프" },
{ ...raw(), subtype: "1년" },
{ ...raw(), pageText: "1/2" },
{ ...raw(), dataCode: "DJI@DJI|US|0" },
{ ...raw(), itemIndex: 1000 }
]) {
assert.equal(workflow.classify(invalid, { id: "foreign_bad", fadeDuration: 6 }), null);
}
assert.equal(workflow.classify(raw(), { id: "foreign_bad", fadeDuration: 6, extra: true }), null);
});
test("only an exact correlated native result materializes s8010 PRICE identity", () => {
const pending = workflow.classify(raw("S&P500", "120일", 7, false), {
id: "foreign_materialized",
fadeDuration: 4
});
const normalized = workflow.normalizeResponse(
response("load_1", [pending]),
"load_1",
[pending]);
assert.ok(normalized);
const entry = workflow.materialize(pending, normalized.rows[0]);
assert.ok(entry);
assert.equal(workflow.isMaterializedEntry(entry), true);
assert.equal(entry.builderKey, "s8010");
assert.equal(entry.code, "8051");
assert.equal(entry.enabled, false);
assert.deepEqual(entry.selection, {
groupCode: "FOREIGN_INDEX",
subject: "S&P500",
graphicType: "",
subtype: "PRICE",
dataCode: "SPI@SPX|US|0"
});
assert.equal(entry.operator.source, "legacy-foreign-index-candle-workflow");
});
test("identity, order, count, correlation and result-shape tampering fail closed", () => {
const first = workflow.classify(raw("다우존스", "5일", 2), {
id: "foreign_first", fadeDuration: 6
});
const second = workflow.classify(raw("나스닥", "20일", 8), {
id: "foreign_second", fadeDuration: 6
});
const pending = [first, second];
const exact = response("load_2", pending);
assert.ok(workflow.normalizeResponse(exact, "load_2", pending));
assert.equal(workflow.normalizeResponse(exact, "stale_load", pending), null);
assert.equal(workflow.normalizeResponse({ ...exact, totalRowCount: 1 }, "load_2", pending), null);
assert.equal(workflow.normalizeResponse({ ...exact, rows: [...exact.rows].reverse() }, "load_2", pending), null);
assert.equal(workflow.normalizeResponse({ ...exact, extra: true }, "load_2", pending), null);
for (const [key, value] of [
["targetKey", "Nasdaq"],
["inputName", "나스닥"],
["symbol", "NAS@IXIC"],
["nationCode", "KR"],
["foreignDomesticCode", "1"],
["sceneCode", "s5001"],
["valueType", "VOLUME"],
["periodDays", 20],
["cutCode", "8040"]
]) {
const rows = [{ ...exact.rows[0], [key]: value }, exact.rows[1]];
assert.equal(workflow.normalizeResponse({ ...exact, rows }, "load_2", pending), null, key);
}
});
test("per-row native failures remain terminal and cannot materialize", () => {
const pending = workflow.classify(raw(), { id: "foreign_failed", fadeDuration: 6 });
for (const failure of [
"INVALID_ROW", "IDENTITY_NOT_FOUND", "IDENTITY_AMBIGUOUS",
"DATABASE_DATA_INVALID", "RESTORE_FAILED"
]) {
const normalized = workflow.normalizeResponse(response("load_3", [pending], [{
itemIndex: pending.itemIndex,
status: "failed",
failure
}]), "load_3", [pending]);
assert.ok(normalized);
assert.equal(workflow.materialize(pending, normalized.rows[0]), null);
}
assert.equal(workflow.normalizeResponse(response("load_3", [pending], [{
itemIndex: pending.itemIndex,
status: "failed",
failure: "RETRY"
}]), "load_3", [pending]), null);
});
test("forged pending and forged resolved values never materialize", () => {
const pending = workflow.classify(raw(), { id: "foreign_trusted", fadeDuration: 6 });
const normalized = workflow.normalizeResponse(response("load_4", [pending]), "load_4", [pending]);
assert.equal(workflow.materialize({ ...pending }, normalized.rows[0]), null);
assert.equal(workflow.materialize(pending, { ...normalized.rows[0] }), null);
});
test("legacy signature round-trips only the exact verified entry", () => {
const pending = workflow.classify(raw("나스닥", "240일", 3, false), {
id: "foreign_round_trip", fadeDuration: 5
});
const normalized = workflow.normalizeResponse(response("load_5", [pending]), "load_5", [pending]);
const entry = workflow.materialize(pending, normalized.rows[0]);
const restored = workflow.restorePlaylistEntry(entry);
assert.ok(restored);
assert.deepEqual(workflow.legacySelectionForEntry(restored), {
groupCode: "해외지수",
subject: "나스닥",
graphicType: "캔들그래프",
subtype: "240일",
dataCode: ""
});
assert.equal(workflow.matchesRaw(restored, raw("나스닥", "240일", 3, false)), true);
assert.equal(workflow.matchesRaw(restored, raw("나스닥", "120일", 3, false)), false);
for (const tampered of [
{ ...entry, code: "8040" },
{ ...entry, selection: { ...entry.selection, subject: "다우존스" } },
{ ...entry, operator: { ...entry.operator, symbol: "DJI@DJI" } },
{ ...entry, operator: { ...entry.operator, periodDays: 120 } },
{ ...entry, extra: true }
]) {
assert.equal(workflow.restorePlaylistEntry(tampered), null);
}
});
test("enabled replacement preserves the branded foreign-index proof immutably", () => {
const storedRaw = raw("S&P500", "20일", 6, true);
const pending = workflow.classify(storedRaw, {
id: "foreign_toggle", fadeDuration: 6
});
const normalized = workflow.normalizeResponse(response("load_toggle", [pending]),
"load_toggle", [pending]);
const entry = workflow.materialize(pending, normalized.rows[0]);
const replacement = workflow.withEnabled(entry, false);
assert.notEqual(replacement, entry);
assert.equal(Object.isFrozen(replacement), true);
assert.equal(entry.enabled, true);
assert.equal(replacement.enabled, false);
assert.equal(workflow.isMaterializedEntry(replacement), true);
assert.equal(workflow.matchesRaw(replacement, { ...storedRaw, enabled: false }), true);
assert.equal(workflow.withEnabled({ ...entry }, false), null);
});
test("bridge errors are request-bound and never retryable", () => {
const error = {
requestId: "load_error",
code: "PLAYOUT_PRIORITY",
message: "송출 우선 처리로 취소했습니다.",
retryable: false
};
assert.deepEqual(workflow.normalizeError(error, "load_error"), error);
assert.equal(workflow.normalizeError({ ...error, retryable: true }, "load_error"), null);
assert.equal(workflow.normalizeError(error, "stale"), null);
assert.equal(workflow.normalizeError({ ...error, code: "RETRY" }, "load_error"), null);
});