181 lines
6.5 KiB
JavaScript
181 lines
6.5 KiB
JavaScript
"use strict";
|
|
|
|
const test = require("node:test");
|
|
const assert = require("node:assert/strict");
|
|
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
|
|
const root = path.resolve(__dirname, "..", "..");
|
|
const app = fs.readFileSync(path.join(root, "Web", "app.js"), "utf8");
|
|
|
|
function functionSource(name, nextName) {
|
|
const start = app.indexOf(`function ${name}(`);
|
|
const end = app.indexOf(`function ${nextName}(`, start + 1);
|
|
assert.ok(start >= 0 && end > start, `${name} must precede ${nextName}`);
|
|
return app.slice(start, end).trim();
|
|
}
|
|
|
|
function evaluateFunction(name, nextName, dependencies = {}) {
|
|
const names = Object.keys(dependencies);
|
|
return new Function(...names,
|
|
`return (${functionSource(name, nextName)});`)(...names.map(key => dependencies[key]));
|
|
}
|
|
|
|
function stateStub() {
|
|
return {
|
|
namedPlaylist: { guardByEntryId: new Map() },
|
|
manualFinancialRestore: { entries: new Map() }
|
|
};
|
|
}
|
|
|
|
test("every restored workflow boundary blocks direct scene and DB-row edits", () => {
|
|
const state = stateStub();
|
|
const blockReason = evaluateFunction(
|
|
"playlistEntryManualEditBlockReason",
|
|
"normalizeManualSceneSelection",
|
|
{ state });
|
|
const protectedEntries = [
|
|
["krx", "legacy-theme-workflow", { namedKrxRestore: {} }],
|
|
["nxt", "legacy-theme-workflow", { namedNxtRestore: {} }],
|
|
["foreign-stock", "legacy-named-foreign-stock-workflow", {}],
|
|
["foreign-index", "legacy-foreign-index-candle-workflow", {}],
|
|
["manual-financial", "manual-financial-workflow", {}],
|
|
["manual-lists", "legacy-manual-lists-workflow", {}]
|
|
];
|
|
for (const [id, source, proof] of protectedEntries) {
|
|
const entry = Object.freeze({
|
|
id,
|
|
enabled: true,
|
|
operator: Object.freeze({ source, ...proof })
|
|
});
|
|
assert.equal(blockReason(entry), "trusted-workflow-entry", id);
|
|
}
|
|
|
|
const guarded = Object.freeze({ id: "guarded", enabled: true });
|
|
state.namedPlaylist.guardByEntryId.set(guarded.id, Object.freeze({ trusted: false }));
|
|
assert.equal(blockReason(guarded), "named-playlist-entry");
|
|
const pending = Object.freeze({ id: "pending", enabled: true });
|
|
state.manualFinancialRestore.entries.set(pending.id, Object.freeze({ status: "load" }));
|
|
assert.equal(blockReason(pending), "manual-restore-entry");
|
|
});
|
|
|
|
test("a frozen ordinary entry receives scene values through an immutable replacement", () => {
|
|
const state = stateStub();
|
|
const blockReason = evaluateFunction(
|
|
"playlistEntryManualEditBlockReason",
|
|
"normalizeManualSceneSelection",
|
|
{ state });
|
|
const normalize = evaluateFunction(
|
|
"normalizeManualSceneSelection",
|
|
"recreatePlaylistEntryWithSceneSelection");
|
|
const sceneAliases = item => item.aliases;
|
|
const recreate = evaluateFunction(
|
|
"recreatePlaylistEntryWithSceneSelection",
|
|
"recreatePlaylistEntryWithLiveSelection",
|
|
{
|
|
playlistEntryManualEditBlockReason: blockReason,
|
|
normalizeManualSceneSelection: normalize,
|
|
sceneAliases
|
|
});
|
|
const entry = Object.freeze({
|
|
id: "ordinary",
|
|
code: "5001",
|
|
aliases: Object.freeze(["5001", "N5001"]),
|
|
enabled: true,
|
|
fadeDuration: 6,
|
|
selection: Object.freeze({
|
|
groupCode: "KOSPI",
|
|
subject: "OLD",
|
|
graphicType: "",
|
|
subtype: "",
|
|
dataCode: ""
|
|
})
|
|
});
|
|
const replacement = recreate(entry, {
|
|
groupCode: " NXT_KOSPI ",
|
|
subject: " 삼성전자 ",
|
|
graphicType: "CURRENT",
|
|
subtype: "",
|
|
dataCode: "005930"
|
|
}, "N5001", 4);
|
|
|
|
assert.notEqual(replacement, entry);
|
|
assert.equal(entry.code, "5001");
|
|
assert.equal(entry.fadeDuration, 6);
|
|
assert.equal(entry.selection.subject, "OLD");
|
|
assert.equal(replacement.code, "N5001");
|
|
assert.equal(replacement.fadeDuration, 4);
|
|
assert.equal(replacement.selection.groupCode, "NXT_KOSPI");
|
|
assert.equal(replacement.selection.subject, "삼성전자");
|
|
assert.equal(Object.isFrozen(replacement.selection), true);
|
|
assert.equal(recreate(entry, replacement.selection, "invalid", 4), null);
|
|
});
|
|
|
|
test("live DB selection also replaces only an unprotected ordinary entry", () => {
|
|
const state = stateStub();
|
|
const blockReason = evaluateFunction(
|
|
"playlistEntryManualEditBlockReason",
|
|
"normalizeManualSceneSelection",
|
|
{ state });
|
|
const normalize = evaluateFunction(
|
|
"normalizeManualSceneSelection",
|
|
"recreatePlaylistEntryWithSceneSelection");
|
|
const recreate = evaluateFunction(
|
|
"recreatePlaylistEntryWithLiveSelection",
|
|
"replacePlaylistEntryAt",
|
|
{
|
|
playlistEntryManualEditBlockReason: blockReason,
|
|
normalizeManualSceneSelection: normalize
|
|
});
|
|
const ordinary = Object.freeze({
|
|
id: "ordinary-live",
|
|
code: "5001",
|
|
enabled: true,
|
|
selection: Object.freeze({
|
|
groupCode: "KOSPI_STOCK",
|
|
subject: "OLD",
|
|
graphicType: "CURRENT",
|
|
subtype: "",
|
|
dataCode: "000001"
|
|
})
|
|
});
|
|
const selection = {
|
|
groupCode: "KOSPI_STOCK",
|
|
subject: "삼성전자",
|
|
graphicType: "CURRENT",
|
|
subtype: "",
|
|
dataCode: "005930"
|
|
};
|
|
const replacement = recreate(ordinary, selection);
|
|
assert.notEqual(replacement, ordinary);
|
|
assert.equal(ordinary.selection.subject, "OLD");
|
|
assert.equal(replacement.selection.subject, "삼성전자");
|
|
assert.equal(replacement.subject, "삼성전자");
|
|
assert.equal(replacement.dataCode, "005930");
|
|
|
|
const protectedEntry = Object.freeze({
|
|
...ordinary,
|
|
id: "protected-live",
|
|
operator: Object.freeze({ source: "legacy-stock-workflow" })
|
|
});
|
|
assert.equal(recreate(protectedEntry, selection), null);
|
|
});
|
|
|
|
test("both edit handlers keep snapshot locking and contain no playlist-entry assignment", () => {
|
|
const render = functionSource("renderSceneSelectionForm", "applySceneSelection");
|
|
const scene = functionSource("applySceneSelection", "applyCutAliasPreset");
|
|
const live = functionSource("applyLiveRowToCurrentCut", "renderLiveTables");
|
|
|
|
assert.match(render, /playlistEntryManualEditBlockReason\(item\)/u);
|
|
assert.match(scene, /if \(!requirePlaylistEditable\(\)\) return;/u);
|
|
assert.match(scene, /playlistEntryManualEditBlockReason\(item\)/u);
|
|
assert.match(scene, /recreatePlaylistEntryWithSceneSelection/u);
|
|
assert.match(scene, /replacePlaylistEntryAt\(state\.currentIndex, replacement\)/u);
|
|
assert.match(live, /if \(!requirePlaylistEditable\(\)\) return;/u);
|
|
assert.match(live, /playlistEntryManualEditBlockReason\(item\)/u);
|
|
assert.match(live, /recreatePlaylistEntryWithLiveSelection/u);
|
|
assert.match(live, /replacePlaylistEntryAt\(state\.currentIndex, replacement\)/u);
|
|
assert.doesNotMatch(app,
|
|
/\bitem\.[A-Za-z_$][A-Za-z0-9_$]*\s*=(?!=)/u);
|
|
});
|