Files
MBN_STOCK_WEBVIEW/tests/Web/named-foreign-index-candle-restore-app-integration.test.cjs

146 lines
6.3 KiB
JavaScript

"use strict";
const test = require("node:test");
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const foreign = require("../../Web/legacy-foreign-index-candle-workflow.js");
const legacyRows = require("../../Web/legacy-named-row-workflow.js");
const namedPlaylists = require("../../Web/named-playlist-workflow.js");
const root = path.resolve(__dirname, "../..");
const app = fs.readFileSync(path.join(root, "Web/app.js"), "utf8");
const index = fs.readFileSync(path.join(root, "Web/index.html"), "utf8");
const bridge = fs.readFileSync(
path.join(root, "MainWindow.NamedForeignIndexCandleRestore.cs"), "utf8");
const namedBridge = fs.readFileSync(path.join(root, "MainWindow.NamedPlaylists.cs"), "utf8");
const core = fs.readFileSync(
path.join(root, "src/MBN_STOCK_WEBVIEW.Core/Data/LegacyForeignIndexCandleRestore.cs"),
"utf8");
function rawRow(period = "240일", enabled = false) {
return {
itemIndex: 0,
enabled,
groupCode: "해외지수",
subject: "다우존스",
graphicType: "캔들그래프",
subtype: period,
pageText: "1/1",
dataCode: ""
};
}
function resolved(pending) {
return {
requestId: "foreign-load-1",
retrievedAt: "2026-07-12T12:34:56+09:00",
totalRowCount: 1,
rows: [{
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
}]
};
}
test("named load performs one correlated exact foreign-index candle batch", () => {
assert.match(index, /<script src="legacy-foreign-index-candle-workflow\.js"><\/script>[\s\S]*<script src="legacy-named-row-workflow\.js"><\/script>/u);
assert.match(app, /MbnLegacyForeignIndexCandleWorkflow/u);
assert.match(app, /foreignIndexCandleRestoreWorkflow\.classify/u);
assert.match(app, /foreignIndexCandleRestoreWorkflow\.normalizeResponse/u);
assert.match(app, /foreignIndexCandleRestoreWorkflow\.materialize/u);
assert.match(app, /case "named-foreign-index-candle-restore-results"/u);
assert.match(app, /case "named-foreign-index-candle-restore-error"/u);
assert.match(namedBridge, /await PostNamedForeignIndexCandleRestoreAsync\(/u);
assert.match(bridge, /PostMessage\("named-foreign-index-candle-restore-results"/u);
assert.match(bridge, /PostMessage\("named-foreign-index-candle-restore-error"/u);
});
test("native and Core retain the full raw boundary and exact bound Oracle identity", () => {
for (const field of [
"item.ItemIndex", "item.IsEnabled", "item.Selection.GroupCode", "item.Selection.Subject",
"item.Selection.GraphicType", "item.Selection.Subtype", "item.Page?.ToString()",
"item.Selection.DataCode"
]) assert.match(bridge, new RegExp(field.replace(/[.?]/g, value => `\\${value}`), "u"));
assert.match(core, /F_INPUT_NAME = :input_name/u);
assert.match(core, /F_SYMB = :symbol/u);
assert.match(core, /F_NATC = 'US'/u);
assert.match(core, /F_FDTC = '0'/u);
assert.match(core, /new DataQueryParameter\("input_name"/u);
assert.match(core, /new DataQueryParameter\("symbol"/u);
assert.doesNotMatch(core, /\b(?:INSERT\s+INTO|UPDATE\s+[A-Z_]|DELETE\s+FROM|MERGE\s+INTO)\b/iu);
assert.doesNotMatch(bridge, /Tornado|TakeIn|PrepareAsync|ExecuteNonQuery/iu);
});
test("restore pending locks edit, page planning and finalization until its terminal result", () => {
assert.match(app, /state\.namedPlaylist\.foreignIndexCandleRestorePending/u);
assert.match(app, /comparisonRestorePending \|\|[\s\S]*foreignIndexCandleRestorePending/u);
assert.match(app, /pendingForeignIndexCandles\.length === 0/u);
assert.match(app, /if \(state\.namedPlaylist\.manualRestoreFinalized\) requestNamedPlaylistPagePlans\(\)/u);
assert.match(app, /foreignIndexCandleRestorePending[\s\S]*state\.manualFinancialRestore\.entries\.size > 0/u);
assert.match(bridge, /PLAYOUT_PRIORITY/u);
assert.match(bridge, /자동 재시도하지 않습니다/u);
assert.match(bridge, /retryable = false/u);
});
test("verified 240-day entry round-trips through the exact legacy named signature", () => {
const raw = rawRow();
const pending = foreign.classify(raw, { id: "db-foreign-240", fadeDuration: 6 });
const response = foreign.normalizeResponse(resolved(pending), "foreign-load-1", [pending]);
const entry = foreign.materialize(pending, response.rows[0]);
assert.ok(entry);
assert.equal(entry.code, "8056");
assert.deepEqual(legacyRows.legacySelectionForEntry(entry), {
groupCode: "해외지수",
subject: "다우존스",
graphicType: "캔들그래프",
subtype: "240일",
dataCode: ""
});
assert.equal(legacyRows.matchesCanonicalEntry(entry, raw), true);
const fields = namedPlaylists.toLegacySevenFields(
{ ...entry, selection: legacyRows.legacySelectionForEntry(entry) },
"1/1");
assert.deepEqual(fields, ["0", "해외지수", "다우존스", "캔들그래프", "240일", "1/1", ""]);
const load = namedPlaylists.normalizeLoadResponse({
requestId: "foreign-load-roundtrip",
definition: { programCode: "00000001", title: "Foreign" },
retrievedAt: "2026-07-12T12:34:56+09:00",
totalRowCount: 1,
canMutate: true,
writeQuarantined: false,
rows: [raw]
});
const restoration = namedPlaylists.restoreRawRows(
load,
() => entry,
legacyRows.matchesCanonicalEntry);
assert.equal(restoration.rows[0].trusted, true);
assert.equal(restoration.rows[0].entry.code, "8056");
});
test("identity and saved-signature tampering remain blocked row by row", () => {
const raw = rawRow("20일", true);
const pending = foreign.classify(raw, { id: "db-foreign-tamper", fadeDuration: 6 });
const response = foreign.normalizeResponse(resolved(pending), "foreign-load-1", [pending]);
const entry = foreign.materialize(pending, response.rows[0]);
assert.equal(legacyRows.matchesCanonicalEntry(entry, raw), true);
assert.equal(legacyRows.matchesCanonicalEntry(
{ ...entry, selection: { ...entry.selection, dataCode: "NAS@IXIC|US|0" } },
raw), false);
assert.equal(legacyRows.matchesCanonicalEntry(entry, { ...raw, subtype: "60일" }), false);
assert.match(app, /MATERIALIZATION_MISMATCH/u);
assert.match(app, /outcome\.status === "failed"/u);
});