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); });