"use strict"; const test = require("node:test"); const assert = require("node:assert/strict"); const workflow = require("../../Web/overseas-workflow.js"); const industry = Object.freeze({ koreanName: "Philadelphia Semiconductor", inputName: "US Semiconductor Index", symbol: "NAS@SOX", nationCode: "US", fdtc: "0" }); const usStock = Object.freeze({ inputName: "NVIDIA", symbol: "NAS@NVDA", nationCode: "US", fdtc: "1" }); const twStock = Object.freeze({ inputName: "Taiwan Semiconductor", symbol: "TWS@2330", nationCode: "TW", fdtc: "1" }); function clone(value) { return JSON.parse(JSON.stringify(value)); } function industryResponse(overrides = {}) { return { requestId: "overseas-industry-1", query: "", retrievedAt: "2026-07-11T12:34:56+09:00", totalRowCount: 2, truncated: false, results: [ { koreanName: "Alpha Industry", inputName: "ALPHA_INDEX", symbol: "NAS@ALPHA", nationCode: "US", fdtc: "0" }, { koreanName: "Beta Industry", inputName: "BETA_INDEX", symbol: "NAS@BETA", nationCode: "US", fdtc: "0" } ], ...overrides }; } function stockResponse(overrides = {}) { return { requestId: "overseas-stock-1", query: "", retrievedAt: "2026-07-11T12:34:56+09:00", totalRowCount: 2, truncated: false, results: [ { inputName: "Apple", symbol: "NAS@AAPL", nationCode: "US", fdtc: "1" }, { inputName: "Taiwan Semiconductor", symbol: "TWS@2330", nationCode: "TW", fdtc: "1" } ], ...overrides }; } test("UC5 bridge contracts expose one industry action and six stock actions", () => { assert.deepEqual(workflow.bridgeContracts.industry, { requestType: "search-overseas-industries", resultType: "overseas-industry-results", errorType: "overseas-industry-error", defaultMaximumResults: 100, maximumResults: 500, maximumQueryLength: 64 }); assert.deepEqual(workflow.bridgeContracts.stock, { requestType: "search-world-stocks", resultType: "world-stock-search-results", errorType: "world-stock-search-error", defaultMaximumResults: 100, maximumResults: 500, maximumQueryLength: 64 }); assert.deepEqual(workflow.industryActions.map(value => [value.id, value.code]), [ ["industry-current", "5001"] ]); assert.deepEqual(workflow.stockActions.map(value => [value.id, value.code, value.periodDays]), [ ["stock-current", "5001", null], ["stock-candle-5d", "8061", 5], ["stock-candle-20d", "8040", 20], ["stock-candle-60d", "8046", 60], ["stock-candle-120d", "8051", 120], ["stock-candle-240d", "8056", 240] ]); assert.deepEqual(workflow.sourceContract.industry, { fdtc: "0", nationCodes: ["US"], lookupField: "F_KNAM", currentCutCode: "5001" }); assert.deepEqual(workflow.sourceContract.stock.candleCutCodes, [ "8061", "8040", "8046", "8051", "8056" ]); }); test("industry and stock search requests normalize blank initial lists and exact limits", () => { assert.deepEqual( workflow.createIndustrySearchRequest("industry-request", " "), { requestId: "industry-request", query: "" }); assert.deepEqual( workflow.createStockSearchRequest("stock-request", " NVDA ", 200), { requestId: "stock-request", query: "NVDA", maximumResults: 200 }); for (const create of [workflow.createIndustrySearchRequest, workflow.createStockSearchRequest]) { assert.throws(() => create("bad id", ""), /safe/i); assert.throws(() => create("request", "bad\nquery"), /safe/i); assert.throws(() => create("request", "x".repeat(65)), /64/); assert.throws(() => create("request", "", 0), /1 through 500/); assert.throws(() => create("request", "", 501), /1 through 500/); assert.throws(() => create("request", "", "100"), /1 through 500/); } }); test("native identities require exact name, symbol, nation and FDTC fields", () => { assert.deepEqual(workflow.normalizeIndustry(industry), industry); assert.deepEqual(workflow.normalizeStock(usStock), usStock); assert.deepEqual(workflow.normalizeStock(twStock), twStock); assert.equal(workflow.normalizeIndustry({ ...industry, nationCode: "TW" }), null); assert.equal(workflow.normalizeIndustry({ ...industry, fdtc: "1" }), null); assert.equal(workflow.normalizeIndustry({ ...industry, koreanName: " Industry" }), null); assert.equal(workflow.normalizeIndustry({ ...industry, inputName: "A|B" }), null); assert.equal(workflow.normalizeStock({ ...usStock, nationCode: "JP" }), null); assert.equal(workflow.normalizeStock({ ...usStock, fdtc: 1 }), null); assert.equal(workflow.normalizeStock({ ...usStock, symbol: "../../NVDA" }), null); assert.equal(workflow.normalizeStock({ ...usStock, inputName: "NV\u200BIDIA" }), null); assert.equal(workflow.normalizeStock({ ...usStock, koreanName: "extra" }), null); }); test("industry responses are request-bound, ordered and deeply normalized", () => { const request = workflow.createIndustrySearchRequest("overseas-industry-1", "", 2); const normalized = workflow.normalizeIndustrySearchResponse(industryResponse(), request); assert.ok(normalized); assert.deepEqual(normalized, industryResponse()); assert.equal(Object.isFrozen(normalized), true); assert.equal(Object.isFrozen(normalized.results), true); assert.equal(Object.isFrozen(normalized.results[0]), true); assert.equal(workflow.normalizeIndustrySearchResponse( industryResponse({ requestId: "stale" }), request), null); assert.equal(workflow.normalizeIndustrySearchResponse( industryResponse({ totalRowCount: 1 }), request), null); assert.equal(workflow.normalizeIndustrySearchResponse( industryResponse({ retrievedAt: "invalid" }), request), null); assert.equal(workflow.normalizeIndustrySearchResponse( { ...industryResponse(), extra: true }, request), null); assert.equal(workflow.normalizeIndustrySearchResponse(industryResponse({ results: [...industryResponse().results].reverse() }), request), null); const overDefault = Array.from({ length: 101 }, (_, index) => { const key = String(index).padStart(3, "0"); return { koreanName: `Industry ${key}`, inputName: `INDEX_${key}`, symbol: `NAS@I${key}`, nationCode: "US", fdtc: "0" }; }); assert.equal(workflow.normalizeIndustrySearchResponse({ ...industryResponse(), totalRowCount: overDefault.length, results: overDefault }, workflow.createIndustrySearchRequest("overseas-industry-1", "")), null); }); test("industry responses reject every ambiguous downstream lookup key", () => { for (const key of ["koreanName", "inputName", "symbol"]) { const rows = clone(industryResponse().results); rows[1][key] = rows[0][key]; assert.equal(workflow.normalizeIndustrySearchResponse(industryResponse({ results: rows })), null); } const wrongScope = clone(industryResponse()); wrongScope.results[1].fdtc = "1"; assert.equal(workflow.normalizeIndustrySearchResponse(wrongScope), null); }); test("stock responses preserve US/TW identity and reject duplicate input-name lookups", () => { const request = workflow.createStockSearchRequest("overseas-stock-1", "", 2); assert.ok(workflow.normalizeStockSearchResponse(stockResponse(), request)); const duplicateName = clone(stockResponse()); duplicateName.results[1].inputName = duplicateName.results[0].inputName; assert.equal(workflow.normalizeStockSearchResponse(duplicateName), null); const sameSymbol = clone(stockResponse()); sameSymbol.results[1].symbol = sameSymbol.results[0].symbol; assert.ok(workflow.normalizeStockSearchResponse(sameSymbol)); const wrongNation = clone(stockResponse()); wrongNation.results[1].nationCode = "JP"; assert.equal(workflow.normalizeStockSearchResponse(wrongNation), null); assert.equal(workflow.normalizeStockSearchResponse(stockResponse({ results: [...stockResponse().results].reverse() })), null); }); test("bridge errors use exact payloads and reject stale request correlation", () => { const industryRequest = workflow.createIndustrySearchRequest("industry-error-1", "AI"); const industryError = { requestId: "industry-error-1", query: "AI", message: "Industry lookup failed." }; assert.deepEqual( workflow.normalizeIndustrySearchError(industryError, industryRequest), industryError); assert.equal(workflow.normalizeIndustrySearchError({ ...industryError, requestId: "stale" }, industryRequest), null); assert.equal(workflow.normalizeIndustrySearchError({ ...industryError, extra: true }, industryRequest), null); const stockRequest = workflow.createStockSearchRequest("stock-error-1", "NVDA"); const stockError = { requestId: "stock-error-1", query: "NVDA", message: "Failed." }; assert.deepEqual(workflow.normalizeStockSearchError(stockError, stockRequest), stockError); assert.equal(workflow.normalizeStockSearchError({ ...stockError, message: "bad\nmessage" }, stockRequest), null); }); test("US overseas industries expose only the 5001 current one-column mapping", () => { assert.equal(workflow.isActionAllowed("industry", "industry-current", industry), true); assert.equal(workflow.isActionAllowed("industry", "stock-candle-5d", industry), false); const mapping = workflow.buildOverseasIndustrySelection(industry); assert.equal(mapping.builderKey, "s5001"); assert.equal(mapping.code, "5001"); assert.deepEqual(mapping.aliases, ["5001"]); assert.deepEqual(mapping.selection, { groupCode: "FOREIGN_INDUSTRY", subject: "Philadelphia Semiconductor", graphicType: "1\uC5F4\uD310\uAE30\uBCF8", subtype: "CURRENT", dataCode: "US Semiconductor Index|NAS@SOX|US|0" }); assert.throws(() => workflow.buildOverseasIndustrySelection({ ...industry, nationCode: "TW" }), /valid/i); }); test("US/TW stocks expose 5001 current and the five exact UC5 candle aliases", () => { const current = workflow.buildOverseasStockSelection(usStock, "stock-current", { ma5: true, ma20: true }); assert.equal(current.builderKey, "s5001"); assert.deepEqual(current.selection, { groupCode: "FOREIGN_STOCK", subject: "NVIDIA", graphicType: "1\uC5F4\uD310\uAE30\uBCF8", subtype: "CURRENT", dataCode: "NAS@NVDA|US|1" }); assert.deepEqual(current.movingAverages, { ma5: false, ma20: false }); for (const action of workflow.stockActions.filter(value => value.kind === "candle")) { const mapping = workflow.buildOverseasStockSelection(twStock, action.id, { ma5: true, ma20: false }); assert.equal(mapping.builderKey, "s8010"); assert.equal(mapping.code, action.code); assert.deepEqual(mapping.aliases, [action.code]); assert.deepEqual(mapping.selection, { groupCode: "FOREIGN_STOCK", subject: "Taiwan Semiconductor", graphicType: "MA5", subtype: "PRICE", dataCode: "TWS@2330|TW|1" }); } assert.throws(() => workflow.buildOverseasStockSelection(usStock, "stock-candle-intraday"), /unknown/i); assert.throws(() => workflow.buildOverseasStockSelection(usStock, "industry-current"), /unknown/i); assert.throws(() => workflow.buildOverseasStockSelection(usStock, "stock-current", { ma5: "true" }), /boolean/i); }); test("playlist entries preserve typed FDTC identity and runtime candle bindings", () => { const industryEntry = workflow.createOverseasIndustryPlaylistEntry(industry, { id: "industry-entry", fadeDuration: 7 }); assert.equal(industryEntry.category, "plate"); assert.equal(industryEntry.symbol, "NAS@SOX"); assert.equal(industryEntry.nationCode, "US"); assert.equal(industryEntry.fdtc, "0"); assert.equal(industryEntry.operator.kind, "industry"); assert.equal(industryEntry.operator.schemaVersion, 1); const candleEntry = workflow.createOverseasStockPlaylistEntry( twStock, "stock-candle-240d", { id: "tw-candle", fadeDuration: 8, ma5: true, ma20: true }); assert.equal(candleEntry.builderKey, "s8010"); assert.equal(candleEntry.code, "8056"); assert.equal(candleEntry.category, "chart"); assert.equal(candleEntry.selection.dataCode, "TWS@2330|TW|1"); assert.equal(candleEntry.selection.graphicType, "MA5,MA20"); assert.equal(candleEntry.operator.kind, "stock"); assert.deepEqual(candleEntry.operator.identity, twStock); }); test("trusted restore reconstructs entries and rejects identity or mapping tampering", () => { const original = workflow.createOverseasStockPlaylistEntry( usStock, "stock-candle-120d", { id: "stored-overseas", fadeDuration: 9, ma5: true, ma20: false }); original.enabled = false; const restored = workflow.restoreOverseasPlaylistEntry(clone(original)); assert.ok(restored); assert.equal(restored.enabled, false); const tampers = [ value => { value.builderKey = "s5001"; }, value => { value.code = "8040"; }, value => { value.selection.groupCode = "FOREIGN_INDEX"; }, value => { value.selection.subject = "Other"; }, value => { value.selection.dataCode = "NAS@NVDA|TW|1"; }, value => { value.selection.subtype = "VOLUME"; }, value => { value.symbol = "NAS@OTHER"; }, value => { value.nationCode = "TW"; }, value => { value.fdtc = "0"; }, value => { value.operator.actionId = "stock-current"; }, value => { value.operator.schemaVersion = 2; }, value => { value.operator.identity.symbol = "NAS@OTHER"; }, value => { value.operator.identity.nationCode = "TW"; }, value => { value.operator.identity.fdtc = "0"; }, value => { value.operator.movingAverages.ma5 = false; }, value => { value.enabled = "false"; } ]; for (const tamper of tampers) { const candidate = clone(original); tamper(candidate); assert.equal(workflow.restoreOverseasPlaylistEntry(candidate), null); } const industryEntry = workflow.createOverseasIndustryPlaylistEntry(industry, { id: "stored-industry" }); assert.ok(workflow.restoreOverseasPlaylistEntry(clone(industryEntry))); const tamperedIndustry = clone(industryEntry); tamperedIndustry.operator.identity.koreanName = "Other Industry"; assert.equal(workflow.restoreOverseasPlaylistEntry(tamperedIndustry), null); assert.equal(workflow.restoreOverseasPlaylistEntry(null), null); }); test("playlist option validation rejects unsafe ids, fades and identities", () => { assert.throws(() => workflow.createOverseasIndustryPlaylistEntry(industry, { id: "bad id" }), /safe/i); assert.throws(() => workflow.createOverseasStockPlaylistEntry(usStock, "stock-current", { id: "fade", fadeDuration: 61 }), /0 through 60/i); assert.throws(() => workflow.createOverseasStockPlaylistEntry(usStock, "stock-current", { id: "flags", ma20: 1 }), /boolean/i); assert.throws(() => workflow.createOverseasStockPlaylistEntry( { ...usStock, nationCode: "JP" }, "stock-current", { id: "invalid" }), /valid/i); });