"use strict"; const test = require("node:test"); const assert = require("node:assert/strict"); const comparison = require("../../Web/comparison-workflow.js"); const workflow = require("../../Web/comparison-import-workflow.js"); const digest = "A".repeat(64); const retrievedAt = "2026-07-12T08:00:00+09:00"; const samsung = Object.freeze({ kind: "krx-stock", market: "kospi", stockName: "삼성전자", stockCode: "005930" }); const nxtSamsung = Object.freeze({ kind: "nxt-stock", market: "kospi", stockName: "삼성전자", stockCode: "005930" }); const nvidia = Object.freeze({ kind: "world-stock", inputName: "엔비디아", symbol: "NAS@NVDA", nation: "US" }); function response(pairs = [ { rowNumber: 1, first: samsung, second: nxtSamsung }, { rowNumber: 2, first: { kind: "market-target", target: "Kosdaq" }, second: nvidia } ]) { return { requestId: "import-request-1", sourceSha256: digest, sourceRowCount: pairs.length, retrievedAt, pairs }; } test("strict import response normalizes all four trusted target shapes", () => { const normalized = workflow.normalizeImportResponse(response(), "import-request-1"); assert.ok(normalized); assert.equal(normalized.sourceRowCount, 2); assert.equal(normalized.pairs[0].pair[0].displayName, "삼성전자"); assert.equal(normalized.pairs[0].pair[1].displayName, "삼성전자(NXT)"); assert.equal(normalized.pairs[1].pair[0], comparison.getMarketTarget("Kosdaq")); assert.deepEqual(normalized.pairs[1].pair[1], { ...nvidia, displayName: "엔비디아" }); assert.ok(Object.isFrozen(normalized)); assert.ok(Object.isFrozen(normalized.pairs)); }); test("response validation rejects correlation, property, digest, row and target tampering", () => { const valid = response(); assert.equal(workflow.normalizeImportResponse(valid, "wrong-request"), null); assert.equal(workflow.normalizeImportResponse({ ...valid, extra: true }, valid.requestId), null); assert.equal(workflow.normalizeImportResponse({ ...valid, sourceSha256: digest.toLowerCase() }, valid.requestId), null); assert.equal(workflow.normalizeImportResponse({ ...valid, sourceRowCount: 3 }, valid.requestId), null); assert.equal(workflow.normalizeImportResponse({ ...valid, pairs: [{ ...valid.pairs[0], rowNumber: 2 }, valid.pairs[1]] }, valid.requestId), null); assert.equal(workflow.normalizeImportResponse({ ...valid, pairs: [{ ...valid.pairs[0], first: { ...samsung, displayName: "삼성전자" } }, valid.pairs[1]] }, valid.requestId), null); assert.equal(workflow.normalizeImportResponse({ ...valid, pairs: [valid.pairs[0], { ...valid.pairs[1], second: { ...nvidia, nation: "KR" } }] }, valid.requestId), null); }); test("duplicate source pairs fail closed before local persistence", () => { const duplicate = response([ { rowNumber: 1, first: samsung, second: nxtSamsung }, { rowNumber: 2, first: samsung, second: nxtSamsung } ]); assert.equal(workflow.normalizeImportResponse(duplicate, duplicate.requestId), null); }); test("merge appends deterministic records without replacing current local pairs", () => { const normalized = workflow.normalizeImportResponse(response(), "import-request-1"); let current = comparison.deleteAllPairs(); current = comparison.appendPair( current, "existing", [comparison.getMarketTarget("Kospi"), samsung]); const merged = workflow.mergeImport(current, normalized); assert.deepEqual(merged.pairs.map(value => value.id), [ "existing", "legacy-compare-aaaaaaaaaaaa-1", "legacy-compare-aaaaaaaaaaaa-2" ]); assert.equal(current.pairs.length, 1); assert.equal(merged.pairs[2].pair[1].symbol, "NAS@NVDA"); }); test("merge rejects duplicate existing pairs and the 500-pair boundary atomically", () => { const normalized = workflow.normalizeImportResponse(response(), "import-request-1"); let duplicate = comparison.deleteAllPairs(); duplicate = comparison.appendPair(duplicate, "already-present", normalized.pairs[0].pair); assert.throws(() => workflow.mergeImport(duplicate, normalized), /existing pair/i); assert.equal(duplicate.pairs.length, 1); let full = comparison.deleteAllPairs(); for (let index = 0; index < 499; index += 1) { full = comparison.appendPair(full, `pair-${index}`, [ comparison.getMarketTarget(index % 2 ? "Kospi" : "Kosdaq"), { ...samsung, stockCode: String(index).padStart(6, "0") } ]); } assert.throws(() => workflow.mergeImport(full, normalized), /cannot fit/i); assert.equal(full.pairs.length, 499); }); test("merge independently rejects unnormalized count and row-order input", () => { const normalized = workflow.normalizeImportResponse(response(), "import-request-1"); const empty = comparison.deleteAllPairs(); assert.throws(() => workflow.mergeImport(empty, { ...normalized, sourceRowCount: 1 }), /cannot fit/i); assert.throws(() => workflow.mergeImport(empty, { ...normalized, pairs: [{ ...normalized.pairs[0], rowNumber: 2 }, normalized.pairs[1]] }), /row order/i); }); test("one-time marker is schema-versioned, strict and digest-bound", () => { const normalized = workflow.normalizeImportResponse(response(), "import-request-1"); const marker = workflow.createMarker(normalized, "2026-07-12T00:00:00.000Z"); assert.deepEqual(marker, { schemaVersion: 1, sourceSha256: digest, importedPairCount: 2, importedAt: "2026-07-12T00:00:00.000Z" }); assert.deepEqual(workflow.normalizeMarker(JSON.parse(JSON.stringify(marker))), marker); assert.equal(workflow.normalizeMarker({ ...marker, schemaVersion: 2 }), null); assert.equal(workflow.normalizeMarker({ ...marker, sourceSha256: "0".repeat(63) }), null); assert.equal(workflow.normalizeMarker({ ...marker, extra: true }), null); }); test("pair list and one-time marker form one strict crash-consistent envelope", () => { const normalized = workflow.normalizeImportResponse(response(), "import-request-1"); const merged = workflow.mergeImport(comparison.deleteAllPairs(), normalized); const marker = workflow.createMarker(normalized, "2026-07-12T00:00:00.000Z"); const envelope = workflow.createStorageEnvelope(merged, marker); assert.ok(envelope); assert.equal(envelope.schemaVersion, workflow.storageEnvelopeSchemaVersion); assert.equal(envelope.pairList.pairs.length, 2); assert.equal(envelope.legacyImportMarker.sourceSha256, digest); assert.deepEqual( workflow.normalizeStorageEnvelope(JSON.parse(JSON.stringify(envelope))), envelope); }); test("a legacy-import record without its marker is fail-closed as a split commit", () => { const normalized = workflow.normalizeImportResponse(response(), "import-request-1"); const merged = workflow.mergeImport(comparison.deleteAllPairs(), normalized); assert.equal(workflow.createStorageEnvelope(merged, null), null); assert.equal(workflow.normalizeStorageEnvelope({ schemaVersion: workflow.storageEnvelopeSchemaVersion, pairList: merged, legacyImportMarker: null }), null); });