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

137 lines
5.6 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 foreignStock = require("../../Web/named-foreign-stock-restore-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.NamedForeignStockRestore.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/LegacyNamedForeignStockRestore.cs"),
"utf8");
function rawRow(enabled = false) {
return {
itemIndex: 0,
enabled,
groupCode: "해외종목",
subject: "NVIDIA",
graphicType: "캔들그래프",
subtype: "240일",
pageText: "1/1",
dataCode: ""
};
}
function materializedEntry() {
const raw = rawRow();
const pending = foreignStock.classify(raw, {
id: "db-foreign-stock-240",
fadeDuration: 6,
ma5: true,
ma20: false
});
const response = foreignStock.normalizeResponse({
requestId: "foreign-stock-load",
retrievedAt: "2026-07-12T12:34:56+09:00",
totalRowCount: 1,
rows: [{
itemIndex: 0,
status: "resolved",
actionId: "stock-candle-240d",
inputName: "NVIDIA",
symbol: "NAS@NVDA",
nationCode: "US",
foreignDomesticCode: "1",
builderKey: "s8010",
cutCode: "8056",
valueType: "PRICE",
periodDays: 240
}]
}, "foreign-stock-load", [pending]);
return foreignStock.materialize(pending, response.rows[0]);
}
test("named load connects one correlated native foreign-stock restore batch end to end", () => {
assert.match(index, /<script src="named-foreign-stock-restore-workflow\.js"><\/script>[\s\S]*<script src="legacy-named-row-workflow\.js"><\/script>/u);
assert.match(app, /MbnNamedForeignStockRestoreWorkflow/u);
assert.match(app, /namedForeignStockRestoreWorkflow\.classify/u);
assert.match(app, /namedForeignStockRestoreWorkflow\.normalizeResponse/u);
assert.match(app, /namedForeignStockRestoreWorkflow\.materialize/u);
assert.match(app, /case "named-foreign-stock-restore-results"/u);
assert.match(app, /case "named-foreign-stock-restore-error"/u);
assert.match(namedBridge, /await PostNamedForeignStockRestoreAsync\(/u);
assert.match(bridge, /PostMessage\("named-foreign-stock-restore-results"/u);
assert.match(bridge, /PostMessage\("named-foreign-stock-restore-error"/u);
});
test("Core uses one exact bound SELECT and native bridge never writes or calls playout", () => {
assert.match(core, /F_INPUT_NAME = :input_name/u);
assert.match(core, /F_FDTC = '1'/u);
assert.match(core, /F_NATC IN \('US', 'TW'\)/u);
assert.match(core, /WHERE ROWNUM <= 2/u);
assert.match(core, /new DataQueryParameter\("input_name"/u);
assert.doesNotMatch(core, /\bLIKE\b/iu);
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);
assert.match(bridge, /retryable = false/u);
assert.match(bridge, /자동 재시도하지 않습니다/u);
});
test("pending state blocks edits and timeout is terminal without an automatic retry", () => {
assert.match(app, /state\.namedPlaylist\.foreignStockRestorePending/u);
assert.match(app, /armNamedForeignStockRestoreTimeout/u);
assert.match(app, /setTimeout\(\(\) => \{[\s\S]*자동 재시도하지 않습니다\.[\s\S]*\}, 30000\)/u);
assert.match(app, /comparisonRestorePending \|\|[\s\S]*foreignStockRestorePending \|\|[\s\S]*foreignIndexCandleRestorePending/u);
assert.match(app, /pendingForeignStocks\.length === 0/u);
assert.match(app, /clearNamedForeignStockRestoreTimeout\(record\)/u);
assert.doesNotMatch(app, /retryNamedForeignStock|requestNamedForeignStockRestore/u);
});
test("verified identity and moving averages preserve the exact original seven fields for save", () => {
const entry = materializedEntry();
const raw = rawRow();
assert.ok(entry);
assert.equal(legacyRows.matchesCanonicalEntry(entry, raw), true);
const selection = legacyRows.legacySelectionForEntry(entry);
assert.deepEqual(selection, {
groupCode: "해외종목",
subject: "NVIDIA",
graphicType: "캔들그래프",
subtype: "240일",
dataCode: ""
});
assert.deepEqual(entry.operator.movingAverages, { ma5: true, ma20: false });
assert.deepEqual(namedPlaylists.toLegacySevenFields({ ...entry, selection }, "1/1"), [
"0", "해외종목", "NVIDIA", "캔들그래프", "240일", "1/1", ""
]);
assert.match(app, /해외종목 항목의 원본 7필드 서명을 안전하게 왕복할 수 없습니다/u);
assert.match(app, /namedForeignStockRestoreWorkflow\.matchesRaw\(restoredForeignStock, rawRow\)/u);
assert.match(app, /item\.operator\?\.source === namedForeignStockRestoreWorkflow\.source\) return item/u);
});
test("canonical FOREIGN_STOCK remains on the existing workflow and cannot collide", () => {
assert.equal(foreignStock.classify({
...rawRow(true),
groupCode: "FOREIGN_STOCK",
graphicType: "MA5",
subtype: "PRICE",
dataCode: "NAS@NVDA|US|1"
}, {
id: "canonical-does-not-route",
fadeDuration: 6,
ma5: true,
ma20: false
}), null);
assert.match(app, /addOverseasNamedCandidates/u);
assert.match(app, /rawRow\.groupCode !== "FOREIGN_STOCK"/u);
});