"use strict"; const test = require("node:test"); const assert = require("node:assert/strict"); const workflow = require("../../Web/named-comparison-restore-workflow.js"); function raw(overrides = {}) { return { itemIndex: 0, enabled: true, groupCode: "코스피,코스닥", subject: "삼성전자,에이비엘바이오", graphicType: "종목별 비교분석_캔들 그래프", subtype: "", pageText: "1/1", dataCode: "", ...overrides }; } function pending(value = raw(), id = "db-comparison-0") { return workflow.classify(value, { id, fadeDuration: 6 }); } function response(rows, requestId = "named-load-1") { return { requestId, retrievedAt: "2026-07-12T12:00:00+09:00", totalRowCount: rows.length, rows }; } test("the exact original action grammar classifies all nine comparison leaves", () => { const cases = [ ["2열판", "", "two-column-current"], ["2열판", "예상체결", "two-column-expected"], ["종목별 비교분석_캔들 그래프", "", "comparison-candle"], ["종목별 비교분석_라인 그래프", "", "comparison-line"], ["종목별 수익률 비교", "5일", "return-5d"], ["종목별 수익률 비교", "1개월", "return-1m"], ["종목별 수익률 비교", "3개월", "return-3m"], ["종목별 수익률 비교", "6개월", "return-6m"], ["종목별 수익률 비교", "12개월", "return-12m"] ]; for (let index = 0; index < cases.length; index += 1) { const [graphicType, subtype, actionId] = cases[index]; const value = pending(raw({ itemIndex: index, groupCode: graphicType === "2열판" ? "지수" : "코스피,코스닥", subject: graphicType === "2열판" ? "코스피 지수,코스닥 지수" : "삼성전자,에이비엘바이오", graphicType, subtype }), `comparison-${index}`); assert.ok(value); assert.equal(value.actionId, actionId); } }); test("market-lost stock rows are submitted but can only normalize as a failed outcome", () => { const value = pending(raw({ groupCode: "종목", subject: "삼성전자,엔비디아", graphicType: "2열판" })); assert.ok(value); const normalized = workflow.normalizeResponse(response([{ itemIndex: 0, status: "failed", failure: "MISSING_MARKET_IDENTITY" }]), "named-load-1", [value]); assert.equal(normalized.rows[0].failure, "MISSING_MARKET_IDENTITY"); assert.equal(workflow.materialize(value, normalized.rows[0]), null); }); test("a correlated fixed-pair result materializes the explicit comparison selection", () => { const value = pending(raw({ groupCode: "지수", subject: "코스피 지수,코스닥 지수", graphicType: "2열판" })); const normalized = workflow.normalizeResponse(response([{ itemIndex: 0, status: "resolved", actionId: "two-column-current", first: { kind: "market-target", target: "Kospi" }, second: { kind: "market-target", target: "Kosdaq" } }]), "named-load-1", [value]); const entry = workflow.materialize(value, normalized.rows[0]); assert.equal(entry.operator.source, "legacy-comparison-workflow"); assert.equal(entry.operator.actionId, "two-column-current"); assert.equal(entry.selection.subject, "Kospi,Kosdaq"); assert.equal(entry.builderKey, "s8018"); assert.equal(entry.enabled, true); }); test("a correlated exact KRX result materializes candle and preserves disabled state", () => { const value = pending(raw({ enabled: false }), "db-comparison-disabled"); const normalized = workflow.normalizeResponse(response([{ itemIndex: 0, status: "resolved", actionId: "comparison-candle", first: { kind: "krx-stock", market: "kospi", stockName: "삼성전자", stockCode: "005930" }, second: { kind: "krx-stock", market: "kosdaq", stockName: "에이비엘바이오", stockCode: "298380" } }]), "named-load-1", [value]); const entry = workflow.materialize(value, normalized.rows[0]); assert.equal(entry.builderKey, "s5026"); assert.equal(entry.operator.pair[0].stockCode, "005930"); assert.equal(entry.selection.dataCode, "KOSPI:005930|KOSDAQ:298380"); assert.equal(entry.enabled, false); }); test("tampered correlation, ordering, action, target, and failure codes are rejected", () => { const first = pending(raw(), "first"); const second = pending(raw({ itemIndex: 1, graphicType: "종목별 비교분석_라인 그래프" }), "second"); const validRows = [ { itemIndex: 0, status: "failed", failure: "IDENTITY_NOT_FOUND" }, { itemIndex: 1, status: "failed", failure: "IDENTITY_AMBIGUOUS" } ]; assert.ok(workflow.normalizeResponse(response(validRows), "named-load-1", [first, second])); assert.equal(workflow.normalizeResponse(response(validRows, "stale"), "named-load-1", [first, second]), null); assert.equal(workflow.normalizeResponse(response([...validRows].reverse()), "named-load-1", [first, second]), null); assert.equal(workflow.normalizeResponse(response([ { ...validRows[0], failure: "GUESS_MARKET" }, validRows[1] ]), "named-load-1", [first, second]), null); const wrongAction = response([{ itemIndex: 0, status: "resolved", actionId: "comparison-line", first: { kind: "krx-stock", market: "kospi", stockName: "삼성전자", stockCode: "005930" }, second: { kind: "krx-stock", market: "kosdaq", stockName: "에이비엘바이오", stockCode: "298380" } }]); assert.equal(workflow.normalizeResponse(wrongAction, "named-load-1", [first]), null); }); test("malformed raw fields and retryable native errors are rejected", () => { assert.equal(pending(raw({ pageText: "1/2" })), null); assert.equal(pending(raw({ dataCode: "005930" })), null); assert.equal(pending(raw({ subject: "삼성전자" })), null); assert.equal(pending(raw({ subject: " 삼성전자,기아" })), null); assert.equal(pending(raw({ subtype: "20일", graphicType: "종목별 수익률 비교" })), null); assert.deepEqual(workflow.normalizeError({ requestId: "named-load-1", code: "DATABASE_UNAVAILABLE", message: "현재 DB를 사용할 수 없습니다.", retryable: false }, "named-load-1"), { requestId: "named-load-1", code: "DATABASE_UNAVAILABLE", message: "현재 DB를 사용할 수 없습니다.", retryable: false }); assert.equal(workflow.normalizeError({ requestId: "named-load-1", code: "DATABASE_UNAVAILABLE", message: "현재 DB를 사용할 수 없습니다.", retryable: true }, "named-load-1"), null); });