311 lines
11 KiB
JavaScript
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);
|
|
});
|