162 lines
6.4 KiB
JavaScript
162 lines
6.4 KiB
JavaScript
const test = require("node:test");
|
|
const assert = require("node:assert/strict");
|
|
const workflow = require("../../Web/named-nxt-theme-restore-workflow.js");
|
|
|
|
function raw(overrides = {}) {
|
|
return {
|
|
itemIndex: 4,
|
|
enabled: true,
|
|
groupCode: "테마_NXT",
|
|
subject: "반도체(NXT)",
|
|
graphicType: "5단 표그래프",
|
|
subtype: "테마-현재가(입력순)",
|
|
pageText: "1/9",
|
|
dataCode: "0123",
|
|
...overrides
|
|
};
|
|
}
|
|
|
|
function resolved(overrides = {}) {
|
|
return {
|
|
itemIndex: 4,
|
|
status: "resolved",
|
|
actionId: "five-row-current",
|
|
builderKey: "s5074",
|
|
cutCode: "5074",
|
|
pageSize: 5,
|
|
sort: "INPUT_ORDER",
|
|
session: "PRE_MARKET",
|
|
themeTitle: "반도체",
|
|
storedThemeCode: "0123",
|
|
currentThemeCode: "87654321",
|
|
previewItemCount: 7,
|
|
liveItemCount: 6,
|
|
previewIsTruncated: false,
|
|
codeWasRemapped: true,
|
|
...overrides
|
|
};
|
|
}
|
|
|
|
function response(row) {
|
|
return {
|
|
requestId: "load-1",
|
|
retrievedAt: "2026-07-12T12:59:00+09:00",
|
|
totalRowCount: 1,
|
|
rows: [row]
|
|
};
|
|
}
|
|
|
|
test("classify routes every exact NXT theme group row for native fail-closed verification", () => {
|
|
const pending = workflow.classify(raw({
|
|
subject: "",
|
|
graphicType: "unsupported",
|
|
subtype: "",
|
|
pageText: "",
|
|
dataCode: ""
|
|
}), { id: "db-4", fadeDuration: 6 });
|
|
assert.equal(workflow.isPending(pending), true);
|
|
assert.equal(pending.rawSelection.groupCode, "테마_NXT");
|
|
assert.equal(workflow.classify(raw({ groupCode: "테마" }), { id: "db-4", fadeDuration: 6 }), null);
|
|
});
|
|
|
|
test("normalizeResponse accepts one exact correlated current identity", () => {
|
|
const pending = workflow.classify(raw(), { id: "db-4", fadeDuration: 6 });
|
|
const result = workflow.normalizeResponse(response(resolved()), "load-1", [pending]);
|
|
assert.equal(result.rows[0].currentThemeCode, "87654321");
|
|
assert.equal(result.rows[0].liveItemCount, 6);
|
|
});
|
|
|
|
test("normalizeResponse rejects expanded, stale, uncorrelated, or impossible identities", () => {
|
|
const pending = workflow.classify(raw(), { id: "db-4", fadeDuration: 6 });
|
|
assert.equal(workflow.normalizeResponse(response({ ...resolved(), extra: true }), "load-1", [pending]), null);
|
|
assert.equal(workflow.normalizeResponse(response(resolved({ storedThemeCode: "999" })), "load-1", [pending]), null);
|
|
assert.equal(workflow.normalizeResponse(response(resolved({ themeTitle: "다른제목" })), "load-1", [pending]), null);
|
|
assert.equal(workflow.normalizeResponse(response(resolved({ liveItemCount: 8 })), "load-1", [pending]), null);
|
|
assert.equal(workflow.normalizeResponse(response(resolved({ previewItemCount: 241, previewIsTruncated: false })), "load-1", [pending]), null);
|
|
});
|
|
|
|
test("failed rows preserve a closed no-retry reason", () => {
|
|
const pending = workflow.classify(raw(), { id: "db-4", fadeDuration: 6 });
|
|
for (const failure of [
|
|
"INVALID_ROW", "UNSUPPORTED_ACTION", "IDENTITY_NOT_FOUND", "IDENTITY_AMBIGUOUS",
|
|
"PREVIEW_EMPTY", "LIVE_ITEMS_EMPTY", "DATABASE_DATA_INVALID", "RESTORE_FAILED"
|
|
]) {
|
|
const result = workflow.normalizeResponse(response({
|
|
itemIndex: 4,
|
|
status: "failed",
|
|
failure
|
|
}), "load-1", [pending]);
|
|
assert.equal(result.rows[0].failure, failure);
|
|
}
|
|
assert.equal(workflow.normalizeError({
|
|
requestId: "load-1",
|
|
code: "DATABASE_UNAVAILABLE",
|
|
message: "재검증 실패",
|
|
retryable: false
|
|
}, "load-1").retryable, false);
|
|
});
|
|
|
|
test("materialize uses only the current code while retaining the old raw identity for lossless save", () => {
|
|
const sourceRaw = raw({ enabled: false });
|
|
const pending = workflow.classify(sourceRaw, { id: "db-4", fadeDuration: 6 });
|
|
const outcome = workflow.normalizeResponse(response(resolved()), "load-1", [pending]).rows[0];
|
|
const entry = workflow.materialize(pending, outcome);
|
|
|
|
assert.equal(workflow.isMaterializedEntry(entry), true);
|
|
assert.equal(entry.enabled, false);
|
|
assert.equal(entry.selection.groupCode, "PAGED_NXT_THEME");
|
|
assert.equal(entry.selection.dataCode, "87654321");
|
|
assert.equal(entry.operator.theme.themeCode, "87654321");
|
|
assert.equal(entry.pagePreview.itemCount, 6);
|
|
assert.deepEqual(workflow.legacySelectionForEntry(entry), {
|
|
groupCode: "테마_NXT",
|
|
subject: "반도체(NXT)",
|
|
graphicType: "5단 표그래프",
|
|
subtype: "테마-현재가(입력순)",
|
|
dataCode: "0123"
|
|
});
|
|
assert.equal(workflow.matchesRaw(entry, sourceRaw), true);
|
|
});
|
|
|
|
test("dynamic NXT session changes exactly at local 13:00 without losing native proof", () => {
|
|
const sourceRaw = raw();
|
|
const pending = workflow.classify(sourceRaw, { id: "db-4", fadeDuration: 6 });
|
|
const outcome = workflow.normalizeResponse(response(resolved()), "load-1", [pending]).rows[0];
|
|
const entry = workflow.materialize(pending, outcome);
|
|
const atTwelve = workflow.refreshDynamicSession(entry, new Date(2026, 6, 12, 12, 59));
|
|
const atThirteen = workflow.refreshDynamicSession(atTwelve, new Date(2026, 6, 12, 13, 0));
|
|
|
|
assert.equal(atTwelve.selection.graphicType, "PRE_MARKET");
|
|
assert.equal(atThirteen.selection.graphicType, "AFTER_MARKET");
|
|
assert.equal(workflow.isMaterializedEntry(atThirteen), true);
|
|
assert.equal(workflow.matchesRaw(atThirteen, sourceRaw), true);
|
|
});
|
|
|
|
test("enabled replacement is immutable and retains NXT proof across session refresh", () => {
|
|
const sourceRaw = raw({ enabled: true });
|
|
const pending = workflow.classify(sourceRaw, { id: "db-toggle", fadeDuration: 6 });
|
|
const outcome = workflow.normalizeResponse(response(resolved({ itemIndex: 4 })),
|
|
"load-1", [pending]).rows[0];
|
|
const entry = workflow.materialize(pending, outcome);
|
|
const replacement = workflow.withEnabled(entry, false);
|
|
const refreshed = workflow.refreshDynamicSession(replacement, new Date(2026, 6, 12, 13, 0));
|
|
|
|
assert.notEqual(replacement, entry);
|
|
assert.equal(Object.isFrozen(replacement), true);
|
|
assert.equal(entry.enabled, true);
|
|
assert.equal(replacement.enabled, false);
|
|
assert.equal(workflow.isMaterializedEntry(replacement), true);
|
|
assert.equal(workflow.matchesRaw(refreshed, { ...sourceRaw, enabled: false }), true);
|
|
assert.equal(workflow.withEnabled({ ...entry }, false), null);
|
|
});
|
|
|
|
test("materialization refuses a failed outcome and non-pending objects", () => {
|
|
const pending = workflow.classify(raw(), { id: "db-4", fadeDuration: 6 });
|
|
assert.equal(workflow.materialize(pending, {
|
|
itemIndex: 4,
|
|
status: "failed",
|
|
failure: "LIVE_ITEMS_EMPTY"
|
|
}), null);
|
|
assert.equal(workflow.materialize({}, resolved()), null);
|
|
});
|