Complete legacy operator UI and playout migration
This commit is contained in:
310
tests/Web/expert-workflow.test.cjs
Normal file
310
tests/Web/expert-workflow.test.cjs
Normal file
@@ -0,0 +1,310 @@
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user