Migrate remaining legacy operator workflows
This commit is contained in:
171
tests/Web/comparison-import-workflow.test.cjs
Normal file
171
tests/Web/comparison-import-workflow.test.cjs
Normal file
@@ -0,0 +1,171 @@
|
||||
"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);
|
||||
});
|
||||
Reference in New Issue
Block a user