Files
MBN_STOCK_WEBVIEW/tests/Web/expert-workflow.test.cjs

311 lines
11 KiB
JavaScript

const test = require("node:test");
const assert = require("node:assert/strict");
const workflow = require("../../Web/expert-workflow.js");
const expert = Object.freeze({
expertCode: "0001",
expertName: "Analyst A",
source: "oracle"
});
function previewRequest(id = "preview-1", maximumItems) {
return workflow.createExpertPreviewRequest(id, expert, maximumItems);
}
function previewPayload(items, overrides = {}) {
return {
requestId: "preview-1",
selection: { expertCode: "0001", expertName: "Analyst A" },
retrievedAt: "2026-07-11T23:10:00+09:00",
totalRowCount: items.length,
truncated: false,
items,
...overrides
};
}
const recommendations = Object.freeze([
Object.freeze({ playIndex: 0, stockCode: "005930", stockName: "Samsung", buyAmount: 70000 }),
Object.freeze({ playIndex: 2, stockCode: "035720", stockName: "Kakao", buyAmount: 42000 })
]);
test("workflow pins the Oracle UC6 reader and one exact 5074 action", () => {
assert.deepEqual(workflow.bridgeContract, {
searchRequestType: "search-experts",
searchResultType: "expert-search-results",
searchErrorType: "expert-search-error",
previewRequestType: "request-expert-preview",
previewResultType: "expert-preview-results",
previewErrorType: "expert-preview-error",
defaultMaximumResults: 100,
maximumResults: 500,
maximumQueryLength: 64,
defaultMaximumPreviewItems: 100,
maximumPreviewItems: 100
});
assert.equal(workflow.sourceContract.provider, "oracle");
assert.equal(workflow.sourceContract.expertTable, "EXPERT_LIST");
assert.equal(workflow.sourceContract.recommendationTable, "RECOMMEND_LIST");
assert.equal(workflow.sourceContract.pageSize, 5);
assert.equal(workflow.sourceContract.maximumPageCount, 20);
assert.deepEqual(workflow.actions, [{
id: "five-row-current",
label: "전문가 추천 · 5단 표그래프 · 현재가",
builderKey: "s5074",
code: "5074",
aliases: ["5074"],
pageSize: 5
}]);
});
test("expert search requests are exact, bounded and normalize the original blank load", () => {
assert.deepEqual(workflow.createExpertSearchRequest("search-1", " "), {
requestId: "search-1",
query: ""
});
assert.deepEqual(workflow.createExpertSearchRequest("search-2", " Kim ", 25), {
requestId: "search-2",
query: "Kim",
maximumResults: 25
});
assert.throws(() => workflow.createExpertSearchRequest("bad id", ""), /request id/);
assert.throws(() => workflow.createExpertSearchRequest("safe", "bad\u0000query"), /query/);
assert.throws(() => workflow.createExpertSearchRequest("safe", "", 0), /limited/);
assert.throws(() => workflow.createExpertSearchRequest("safe", "", 501), /limited/);
});
test("search responses keep typed code and name and reject ambiguity or reordering", () => {
const request = workflow.createExpertSearchRequest("search-1", "", 5);
const payload = {
requestId: "search-1",
query: "",
retrievedAt: "2026-07-11T23:00:00+09:00",
totalRowCount: 2,
truncated: false,
results: [
{ expertCode: "0001", expertName: "Alpha", source: "oracle" },
{ expertCode: "0002", expertName: "Beta", source: "oracle" }
]
};
const normalized = workflow.normalizeExpertSearchResponse(payload, request);
assert.ok(normalized);
assert.deepEqual(normalized.results[0], {
expertCode: "0001",
expertName: "Alpha",
source: "oracle"
});
assert.ok(Object.isFrozen(normalized.results));
assert.equal(workflow.normalizeExpertSearchResponse({
...payload,
results: [...payload.results].reverse()
}, request), null);
assert.equal(workflow.normalizeExpertSearchResponse({
...payload,
results: [payload.results[0], { ...payload.results[1], expertName: "Alpha" }]
}, request), null);
assert.equal(workflow.normalizeExpertSearchResponse({
...payload,
results: [payload.results[0], { ...payload.results[1], expertCode: "0001" }]
}, request), null);
assert.equal(workflow.normalizeExpertSearchResponse({
...payload,
results: [{ ...payload.results[0], source: "mariaDb" }, payload.results[1]]
}, request), null);
assert.equal(workflow.normalizeExpertSearchResponse({ ...payload, requestId: "stale" }, request), null);
});
test("search errors are exact and correlated to the active request", () => {
const request = workflow.createExpertSearchRequest("search-1", "Kim");
assert.deepEqual(workflow.normalizeExpertSearchError({
requestId: "search-1",
query: "Kim",
message: "Database unavailable"
}, request), {
requestId: "search-1",
query: "Kim",
message: "Database unavailable"
});
assert.equal(workflow.normalizeExpertSearchError({
requestId: "other",
query: "Kim",
message: "Database unavailable"
}, request), null);
assert.equal(workflow.normalizeExpertSearchError({
requestId: "search-1",
query: "Kim",
message: "bad\nmessage"
}, request), null);
});
test("preview requests bind the exact expert code and name", () => {
assert.deepEqual(previewRequest("preview-1", 25), {
requestId: "preview-1",
expertCode: "0001",
expertName: "Analyst A",
maximumItems: 25
});
assert.throws(() => workflow.createExpertPreviewRequest("preview", {
expertCode: "0001",
expertName: "Analyst A",
source: "mariaDb"
}), /Oracle expert/);
assert.throws(() => workflow.createExpertPreviewRequest("preview", {
expertCode: "001",
expertName: "Analyst A",
source: "oracle"
}), /Oracle expert/);
assert.throws(() => previewRequest("preview", 101), /limited/);
});
test("preview responses preserve PLAYINDEX and reject duplicate or unsafe rows", () => {
const request = previewRequest();
const normalized = workflow.normalizeExpertPreviewResponse(
previewPayload(recommendations),
request);
assert.ok(normalized);
assert.deepEqual(normalized.items, recommendations);
assert.ok(Object.isFrozen(normalized.items));
for (const items of [
[recommendations[0], { ...recommendations[1], playIndex: 0 }],
[recommendations[0], { ...recommendations[1], stockCode: "005930" }],
[recommendations[0], { ...recommendations[1], stockName: "Samsung" }],
[recommendations[1], recommendations[0]],
[{ ...recommendations[0], buyAmount: 0 }],
[{ ...recommendations[0], buyAmount: 100.5 }],
[{ ...recommendations[0], stockCode: "../005930" }]
]) {
assert.equal(workflow.normalizeExpertPreviewResponse(previewPayload(items), request), null);
}
assert.equal(workflow.normalizeExpertPreviewResponse(
previewPayload(recommendations, {
selection: { expertCode: "0002", expertName: "Analyst A" }
}), request), null);
});
test("only a native-normalized preview can create the closed PAGED_EXPERT selection", () => {
const raw = previewPayload(recommendations);
assert.equal(workflow.createSelectableExpert(expert, raw), null);
const preview = workflow.normalizeExpertPreviewResponse(raw, previewRequest());
const selected = workflow.createSelectableExpert(expert, preview);
assert.deepEqual(selected, {
expertCode: "0001",
expertName: "Analyst A",
source: "oracle",
itemCount: 2,
previewTruncated: false
});
assert.deepEqual(workflow.buildExpertSelection(selected), {
builderKey: "s5074",
code: "5074",
aliases: ["5074"],
selection: {
groupCode: "PAGED_EXPERT",
subject: "Analyst A",
graphicType: "INPUT_ORDER",
subtype: "CURRENT",
dataCode: "0001"
}
});
assert.deepEqual(workflow.calculatePagePreview(selected), {
itemCount: 2,
accessibleItemCount: 2,
pageSize: 5,
pageCount: 1,
maximumPageCount: 20,
isTruncated: false
});
});
test("the 5 by 20 boundary is explicit and empty experts cannot create a cut", () => {
const hundred = Array.from({ length: 100 }, (_, index) => ({
playIndex: index,
stockCode: String(index + 1).padStart(6, "0"),
stockName: `Stock ${String(index + 1).padStart(3, "0")}`,
buyAmount: 1000 + index
}));
const request = previewRequest("preview-1", 100);
const preview = workflow.normalizeExpertPreviewResponse(
previewPayload(hundred, { truncated: true }),
request);
const selected = workflow.createSelectableExpert(expert, preview);
assert.deepEqual(workflow.calculatePagePreview(selected), {
itemCount: 100,
accessibleItemCount: 100,
pageSize: 5,
pageCount: 20,
maximumPageCount: 20,
isTruncated: true
});
const emptyPreview = workflow.normalizeExpertPreviewResponse(
previewPayload([]),
previewRequest());
const empty = workflow.createSelectableExpert(expert, emptyPreview);
assert.throws(() => workflow.createExpertPlaylistEntry(empty, { id: "empty" }), /no recommendation/);
});
test("playlist creation and trusted restore bind every identity and page field", () => {
const preview = workflow.normalizeExpertPreviewResponse(
previewPayload(recommendations),
previewRequest());
const selected = workflow.createSelectableExpert(expert, preview);
const entry = workflow.createExpertPlaylistEntry(selected, {
id: "expert-1",
fadeDuration: 8
});
assert.equal(entry.builderKey, "s5074");
assert.equal(entry.code, "5074");
assert.equal(entry.pageSize, 5);
assert.equal(entry.pageCount, 1);
assert.equal(entry.fadeDuration, 8);
assert.equal(entry.operator.schemaVersion, 1);
const stored = JSON.parse(JSON.stringify(entry));
stored.enabled = false;
const restored = workflow.restoreExpertPlaylistEntry(stored);
assert.ok(restored);
assert.equal(restored.enabled, false);
assert.equal(restored.selection.dataCode, "0001");
for (const tamper of [
value => { value.builderKey = "s5077"; },
value => { value.code = "5077"; },
value => { value.selection.subject = "Other"; },
value => { value.selection.dataCode = "0002"; },
value => { value.operator.expert.expertName = "Other"; },
value => { value.operator.pagePreview.pageCount = 2; },
value => { value.operator.actionId = "unknown"; },
value => { value.enabled = "false"; }
]) {
const candidate = JSON.parse(JSON.stringify(entry));
tamper(candidate);
assert.equal(workflow.restoreExpertPlaylistEntry(candidate), null);
}
assert.throws(() => workflow.createExpertPlaylistEntry(selected, { id: "bad id" }), /options/);
assert.throws(() => workflow.createExpertPlaylistEntry(selected, { id: "safe", fadeDuration: 61 }), /Fade/);
});
test("preview errors are exact and bound to expert identity", () => {
const request = previewRequest();
assert.deepEqual(workflow.normalizeExpertPreviewError({
requestId: "preview-1",
selection: { expertCode: "0001", expertName: "Analyst A" },
message: "Preview unavailable"
}, request), {
requestId: "preview-1",
selection: { expertCode: "0001", expertName: "Analyst A" },
message: "Preview unavailable"
});
assert.equal(workflow.normalizeExpertPreviewError({
requestId: "preview-1",
selection: { expertCode: "0002", expertName: "Analyst A" },
message: "Preview unavailable"
}, request), null);
assert.equal(workflow.normalizeExpertPreviewError({
requestId: "preview-1",
selection: { expertCode: "0001", expertName: "Analyst A" },
message: "bad\u200Bmessage"
}, request), null);
});