Files
MBN_STOCK_WEBVIEW/tests/Web/named-krx-theme-restore-workflow.test.cjs

250 lines
9.4 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 workflow = require("../../Web/named-krx-theme-restore-workflow.js");
function raw(overrides = {}) {
return {
itemIndex: 4,
enabled: true,
groupCode: "테마",
subject: "반도체",
graphicType: "5단 표그래프",
subtype: "테마-현재가(입력순)",
pageText: "1/9",
dataCode: "0123",
...overrides
};
}
function resolved(overrides = {}) {
return {
itemIndex: 4,
status: "resolved",
actionId: "five-row-current",
builderKey: "s5074",
cutCode: "5074",
pageSize: 5,
sort: "INPUT_ORDER",
themeTitle: "반도체",
storedThemeCode: "0123",
currentThemeCode: "87654321",
previewItemCount: 7,
previewIsTruncated: false,
codeWasRemapped: true,
...overrides
};
}
function response(row) {
return {
requestId: "load-1",
retrievedAt: "2026-07-12T12:59:00+09:00",
totalRowCount: 1,
rows: [row]
};
}
function normalize(sourceRaw = raw(), outcome = resolved()) {
const pending = workflow.classify(sourceRaw, { id: "db-4", fadeDuration: 6 });
const normalized = workflow.normalizeResponse(response(outcome), "load-1", [pending]);
return { pending, normalized, outcome: normalized?.rows[0] ?? null };
}
test("classify routes every exact KRX theme group row for native fail-closed verification", () => {
const pending = workflow.classify(raw({
subject: "",
graphicType: "unsupported",
subtype: "",
pageText: "",
dataCode: ""
}), { id: "db-4", fadeDuration: 6 });
assert.equal(workflow.isPending(pending), true);
assert.equal(pending.rawSelection.groupCode, "테마");
assert.equal(workflow.classify(raw({ groupCode: "테마_NXT" }), {
id: "db-4",
fadeDuration: 6
}), null);
});
test("normalizeResponse accepts one exact correlated current identity", () => {
const { normalized } = normalize();
assert.equal(normalized.rows[0].currentThemeCode, "87654321");
assert.equal(normalized.rows[0].previewItemCount, 7);
assert.deepEqual(workflow.bridgeContract, {
resultType: "named-krx-theme-restore-results",
errorType: "named-krx-theme-restore-error"
});
});
test("normalizeResponse enforces exact wire schema, raw action correlation and preview bounds", () => {
const pending = workflow.classify(raw(), { id: "db-4", fadeDuration: 6 });
for (const invalid of [
{ ...resolved(), extra: true },
resolved({ storedThemeCode: "999" }),
resolved({ themeTitle: "다른제목" }),
resolved({ actionId: "six-row-current", builderKey: "s5077", cutCode: "5077", pageSize: 6 }),
resolved({ sort: "GAIN_ASC" }),
resolved({ previewItemCount: 0 }),
resolved({ previewItemCount: 239, previewIsTruncated: true }),
resolved({ previewItemCount: 241, previewIsTruncated: true }),
resolved({ codeWasRemapped: false })
]) {
assert.equal(workflow.normalizeResponse(response(invalid), "load-1", [pending]), null);
}
assert.equal(workflow.normalizeResponse({
requestId: "load-1",
retrievedAt: "2026-07-12T12:59:00+09:00",
totalRowCount: 2,
rows: [resolved({ itemIndex: 5 }), resolved({ itemIndex: 4 })]
}, "load-1", [
workflow.classify(raw({ itemIndex: 4 }), { id: "db-4", fadeDuration: 6 }),
workflow.classify(raw({ itemIndex: 5 }), { id: "db-5", fadeDuration: 6 })
]), null);
});
test("failed rows preserve a closed no-retry reason", () => {
const pending = workflow.classify(raw(), { id: "db-4", fadeDuration: 6 });
for (const failure of [
"INVALID_ROW", "UNSUPPORTED_ACTION", "IDENTITY_NOT_FOUND", "IDENTITY_AMBIGUOUS",
"PREVIEW_EMPTY", "DATABASE_DATA_INVALID", "RESTORE_FAILED"
]) {
const result = workflow.normalizeResponse(response({
itemIndex: 4,
status: "failed",
failure
}), "load-1", [pending]);
assert.equal(result.rows[0].failure, failure);
}
assert.equal(workflow.normalizeError({
requestId: "load-1",
code: "DATABASE_UNAVAILABLE",
message: "조회 실패",
retryable: false
}, "load-1").retryable, false);
assert.equal(workflow.normalizeError({
requestId: "load-1",
code: "DATABASE_UNAVAILABLE",
message: "조회 실패",
retryable: true
}, "load-1"), null);
});
test("materialize uses only current code and retains the old raw identity for lossless save", () => {
const sourceRaw = raw({ enabled: false });
const { pending, outcome } = normalize(sourceRaw);
const entry = workflow.materialize(pending, outcome);
assert.equal(workflow.isMaterializedEntry(entry), true);
assert.equal(entry.enabled, false);
assert.equal(entry.selection.groupCode, "PAGED_THEME");
assert.equal(entry.selection.dataCode, "87654321");
assert.equal(entry.operator.theme.themeCode, "87654321");
assert.equal(entry.pagePreview.itemCount, 7);
assert.deepEqual(workflow.legacySelectionForEntry(entry), {
groupCode: "테마",
subject: "반도체",
graphicType: "5단 표그래프",
subtype: "테마-현재가(입력순)",
dataCode: "0123"
});
assert.equal(workflow.matchesRaw(entry, sourceRaw), true);
assert.equal(workflow.matchesRaw(entry, { ...sourceRaw, dataCode: "87654321" }), false);
});
test("expected price and the original no-token loss-rate branch remain closed", () => {
const sourceRaw = raw({
subtype: "테마-예상체결가",
pageText: "1/2"
});
const native = resolved({
actionId: "five-row-expected",
sort: "GAIN_ASC"
});
const { pending, outcome } = normalize(sourceRaw, native);
const entry = workflow.materialize(pending, outcome);
assert.equal(entry.operator.actionId, "five-row-expected");
assert.equal(entry.operator.sort, "GAIN_ASC");
assert.equal(entry.selection.subtype, "EXPECTED");
assert.equal(entry.selection.graphicType, "GAIN_ASC");
assert.equal(workflow.matchesRaw(entry, sourceRaw), true);
});
test("5-current, 5-expected, 6 and 12 row wire profiles retain the final sort fallback", () => {
const cases = [
["5단 표그래프", "테마-현재가", "five-row-current", "s5074", "5074", 5],
["5단 표그래프", "테마-예상체결가", "five-row-expected", "s5074", "5074", 5],
["6종목 현재가", "테마-현재가", "six-row-current", "s5077", "5077", 6],
["12종목 현재가", "테마-현재가", "twelve-row-current", "s5088", "5088", 12]
];
for (const [graphicType, subtype, actionId, builderKey, cutCode, pageSize] of cases) {
const sourceRaw = raw({ graphicType, subtype });
const native = resolved({ actionId, builderKey, cutCode, pageSize, sort: "GAIN_ASC" });
const { pending, outcome } = normalize(sourceRaw, native);
const entry = workflow.materialize(pending, outcome);
assert.equal(entry.builderKey, builderKey);
assert.equal(entry.pageSize, pageSize);
assert.equal(entry.operator.sort, "GAIN_ASC");
}
});
test("240 visible rows with truncation is the only accepted truncated preview boundary", () => {
const { pending, outcome } = normalize(raw(), resolved({
previewItemCount: 240,
previewIsTruncated: true
}));
const entry = workflow.materialize(pending, outcome);
assert.equal(entry.operator.namedKrxRestore.previewItemCount, 240);
assert.equal(entry.operator.namedKrxRestore.previewIsTruncated, true);
assert.equal(entry.pagePreview.pageCount, 20);
assert.equal(entry.pagePreview.isTruncated, true);
});
test("materialization accepts only branded normalized responses", () => {
const pending = workflow.classify(raw(), { id: "db-4", fadeDuration: 6 });
assert.equal(workflow.materialize(pending, resolved()), null);
const failed = workflow.normalizeResponse(response({
itemIndex: 4,
status: "failed",
failure: "PREVIEW_EMPTY"
}), "load-1", [pending]).rows[0];
assert.equal(workflow.materialize(pending, failed), null);
assert.equal(workflow.materialize({}, resolved()), null);
});
test("enabled replacement is immutable and preserves only the trusted KRX brand", () => {
const sourceRaw = raw({ enabled: true });
const { pending, outcome } = normalize(sourceRaw);
const entry = workflow.materialize(pending, outcome);
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.deepEqual(workflow.legacySelectionForEntry(replacement),
workflow.legacySelectionForEntry(entry));
assert.equal(workflow.matchesRaw(replacement, { ...sourceRaw, enabled: false }), true);
assert.equal(workflow.withEnabled({ ...entry }, false), null);
assert.equal(workflow.withEnabled(entry, "false"), null);
});
test("native partial emits the exact result and non-retry error contract", () => {
const root = path.resolve(__dirname, "..", "..");
const native = fs.readFileSync(path.join(root, "MainWindow.NamedKrxThemeRestore.cs"), "utf8");
assert.match(native, /PostMessage\("named-krx-theme-restore-results"/u);
assert.match(native, /PostMessage\("named-krx-theme-restore-error"/u);
assert.match(native, /identity\.StoredThemeCode/u);
assert.match(native, /identity\.CurrentThemeCode/u);
assert.match(native, /identity\.PreviewIsTruncated/u);
assert.match(native, /retryable = false/u);
assert.doesNotMatch(native, /retryable = true/u);
assert.doesNotMatch(native, /IPlayoutEngine|PrepareAsync|TakeInAsync|NextAsync|TakeOutAsync/u);
});