243 lines
8.7 KiB
JavaScript
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);
|
|
});
|