741 lines
25 KiB
JavaScript
741 lines
25 KiB
JavaScript
const test = require("node:test");
|
|
const assert = require("node:assert/strict");
|
|
const workflow = require("../../Web/manual-financial-workflow.js");
|
|
|
|
const version = "A".repeat(64);
|
|
|
|
function quarters() {
|
|
return [
|
|
{ quarter: "1Q", value: 10 },
|
|
{ quarter: "2Q", value: -20 },
|
|
{ quarter: "3Q", value: 30 },
|
|
{ quarter: "4Q", value: 40 },
|
|
{ quarter: "5Q", value: 50 },
|
|
{ quarter: "6Q", value: 60 }
|
|
];
|
|
}
|
|
|
|
function record(screen, stockName = "Alpha") {
|
|
switch (screen) {
|
|
case "revenue-composition":
|
|
return {
|
|
kind: screen,
|
|
stockName,
|
|
baseDate: "2026-06",
|
|
slices: [
|
|
{ label: "Semiconductor", percentageText: "50.0", percentage: 50 },
|
|
{ label: "Service", percentageText: "-", percentage: 0 },
|
|
{ label: "Overseas", percentageText: "25", percentage: 25 },
|
|
null,
|
|
null
|
|
]
|
|
};
|
|
case "growth-metrics":
|
|
return {
|
|
kind: screen,
|
|
stockName,
|
|
salesGrowth: [1, 2, 3, 4],
|
|
operatingProfitGrowth: [5, 6, null, 8],
|
|
netAssetGrowth: [9, 10, 11, 12],
|
|
netIncomeGrowth: [13, 14, 15, 16],
|
|
periods: ["1Q", "2Q", "3Q", "4Q"]
|
|
};
|
|
case "sales":
|
|
case "operating-profit":
|
|
return { kind: screen, stockName, quarters: quarters() };
|
|
default:
|
|
throw new Error("bad screen");
|
|
}
|
|
}
|
|
|
|
function listRequest(screen = "sales") {
|
|
return workflow.createListRequest("list-1", screen, "Alpha", 25);
|
|
}
|
|
|
|
function listPayload(screen = "sales", records = [record(screen)]) {
|
|
return {
|
|
requestId: "list-1",
|
|
screen,
|
|
query: "Alpha",
|
|
retrievedAt: "2026-07-12T00:10:00+09:00",
|
|
totalRowCount: records.length,
|
|
truncated: false,
|
|
items: records.map((item, index) => ({
|
|
stockName: item.stockName,
|
|
rowVersion: String.fromCharCode(65 + index).repeat(64),
|
|
record: item
|
|
}))
|
|
};
|
|
}
|
|
|
|
function trustedSnapshot(screen = "sales", stockName = "Alpha") {
|
|
const request = workflow.createListRequest("list-1", screen, "Alpha", 25);
|
|
const response = workflow.normalizeListResponse(
|
|
listPayload(screen, [record(screen, stockName)]),
|
|
request);
|
|
assert.ok(response);
|
|
return response.items[0];
|
|
}
|
|
|
|
function stockPayload(results = [{
|
|
market: "kospi",
|
|
source: "oracle",
|
|
name: "Alpha",
|
|
code: "000001"
|
|
}]) {
|
|
return {
|
|
requestId: "stock-1",
|
|
query: "Alpha",
|
|
retrievedAt: "2026-07-12T00:11:00+09:00",
|
|
totalRowCount: results.length,
|
|
truncated: false,
|
|
results
|
|
};
|
|
}
|
|
|
|
function verifiedStock(recordValue = record("sales"), results) {
|
|
const response = workflow.normalizeStockSearchResponse(stockPayload(results));
|
|
assert.ok(response);
|
|
const verified = workflow.verifyStockForRecord(
|
|
recordValue,
|
|
response,
|
|
"kospi",
|
|
"000001");
|
|
assert.ok(verified);
|
|
return verified;
|
|
}
|
|
|
|
function trustedSelection(snapshot, stock, requestId = "selection-1") {
|
|
const request = workflow.createSelectionRequest(requestId, snapshot, stock);
|
|
const profile = workflow.getScreenDefinition(snapshot.screen);
|
|
const groupCode = {
|
|
kospi: "KOSPI",
|
|
kosdaq: "KOSDAQ",
|
|
"nxt-kospi": "NXT_KOSPI",
|
|
"nxt-kosdaq": "NXT_KOSDAQ"
|
|
}[stock.market];
|
|
const response = workflow.normalizeSelectionResponse({
|
|
requestId,
|
|
screen: snapshot.screen,
|
|
stockName: snapshot.stockName,
|
|
rowVersion: snapshot.rowVersion,
|
|
builderKey: profile.builderKey,
|
|
code: profile.code,
|
|
aliases: [profile.code],
|
|
selection: {
|
|
groupCode,
|
|
subject: snapshot.stockName,
|
|
graphicType: profile.graphicType,
|
|
subtype: "",
|
|
dataCode: ""
|
|
},
|
|
currentPage: 1,
|
|
totalPages: 1
|
|
}, request);
|
|
assert.ok(response);
|
|
return response;
|
|
}
|
|
|
|
function trustedLoad(snapshot, requestId = "load-1") {
|
|
const request = workflow.createLoadRequest(requestId, snapshot.screen, snapshot.stockName);
|
|
const response = workflow.normalizeLoadResponse({
|
|
requestId,
|
|
screen: snapshot.screen,
|
|
retrievedAt: "2026-07-12T00:20:00+09:00",
|
|
snapshot: {
|
|
stockName: snapshot.stockName,
|
|
rowVersion: snapshot.rowVersion,
|
|
record: snapshot.record
|
|
}
|
|
}, request);
|
|
assert.ok(response);
|
|
return response;
|
|
}
|
|
|
|
test("workflow pins GraphE Oracle tables, CRUD bridge and four scene actions", () => {
|
|
assert.deepEqual(workflow.bridgeContract, {
|
|
requestErrorType: "manual-financial-request-error",
|
|
listRequestType: "request-manual-financial-list",
|
|
listResultType: "manual-financial-list-results",
|
|
listErrorType: "manual-financial-list-error",
|
|
loadRequestType: "request-manual-financial-load",
|
|
loadResultType: "manual-financial-load-results",
|
|
loadErrorType: "manual-financial-load-error",
|
|
selectionRequestType: "request-manual-financial-selection",
|
|
selectionResultType: "manual-financial-selection-results",
|
|
selectionErrorType: "manual-financial-selection-error",
|
|
createRequestType: "create-manual-financial-record",
|
|
updateRequestType: "update-manual-financial-record",
|
|
deleteRequestType: "delete-manual-financial-record",
|
|
deleteAllRequestType: "delete-all-manual-financial-records",
|
|
mutationResultType: "manual-financial-mutation-result",
|
|
mutationErrorType: "manual-financial-mutation-error",
|
|
defaultMaximumResults: 200,
|
|
maximumResults: 1000,
|
|
maximumQueryLength: 64
|
|
});
|
|
assert.equal(workflow.sourceContract.originalForm, "Form/GraphE.cs");
|
|
assert.equal(workflow.sourceContract.provider, "oracle");
|
|
assert.equal(workflow.sourceContract.operatorSchemaVersion, 1);
|
|
assert.equal(workflow.sourceContract.restoreRequiresNativeLoad, true);
|
|
assert.equal(workflow.sourceContract.rawNamedPlaylistRowPlayable, false);
|
|
assert.equal(workflow.sourceContract.writeSafety.outcomeUnknownAutomaticRetry, false);
|
|
assert.deepEqual(workflow.actions.map(action => [action.screen, action.builderKey, action.code]), [
|
|
["revenue-composition", "s5076", "5076"],
|
|
["growth-metrics", "s5079", "5079"],
|
|
["sales", "s5080", "5080"],
|
|
["operating-profit", "s5081", "5081"]
|
|
]);
|
|
});
|
|
|
|
test("generic native request errors are exact and operation-correlated", () => {
|
|
const error = {
|
|
requestId: "",
|
|
operation: "load",
|
|
code: "INVALID_REQUEST",
|
|
message: "Invalid screen."
|
|
};
|
|
assert.deepEqual(workflow.normalizeRequestError(error, "load"), error);
|
|
assert.equal(workflow.normalizeRequestError(error, "list"), null);
|
|
assert.equal(workflow.normalizeRequestError({ ...error, extra: true }, "load"), null);
|
|
assert.equal(workflow.normalizeRequestError({ ...error, requestId: "bad id" }, "load"), null);
|
|
assert.equal(workflow.normalizeRequestError({ ...error, operation: "unknown" }), null);
|
|
assert.equal(workflow.normalizeRequestError({ ...error, code: "OTHER" }), null);
|
|
assert.equal(workflow.normalizeRequestError({ ...error, message: "bad\nmessage" }), null);
|
|
});
|
|
|
|
test("all closed records serialize exactly to the existing underscore loader shapes", () => {
|
|
assert.deepEqual(workflow.serializeRecordForStorage(
|
|
"revenue-composition",
|
|
record("revenue-composition")), {
|
|
table: "INPUT_PIE",
|
|
columns: ["STOCK_NAME", "BASE_DATE", "GUSUNG_1", "GUSUNG_2", "GUSUNG_3", "GUSUNG_4", "GUSUNG_5"],
|
|
values: ["Alpha", "2026-06", "Semiconductor_50.0", "Service_-", "Overseas_25", "", ""]
|
|
});
|
|
assert.deepEqual(workflow.serializeRecordForStorage(
|
|
"growth-metrics",
|
|
record("growth-metrics")), {
|
|
table: "INPUT_GROW",
|
|
columns: ["STOCK_NAME", "GUSUNG_1", "GUSUNG_2", "GUSUNG_3", "GUSUNG_4", "BASE_DATE"],
|
|
values: ["Alpha", "1_2_3_4", "5_6__8", "9_10_11_12", "13_14_15_16", "1Q_2Q_3Q_4Q"]
|
|
});
|
|
assert.deepEqual(workflow.serializeRecordForStorage("sales", record("sales")).values, [
|
|
"Alpha", "1Q_10", "2Q_-20", "3Q_30", "4Q_40", "5Q_50", "6Q_60"
|
|
]);
|
|
assert.equal(workflow.serializeRecordForStorage(
|
|
"operating-profit",
|
|
record("operating-profit")).table, "INPUT_PROFIT");
|
|
});
|
|
|
|
test("closed DTO validation rejects malformed counts, separators and unsafe numbers", () => {
|
|
const revenue = record("revenue-composition");
|
|
assert.equal(workflow.normalizeRecord("revenue-composition", {
|
|
...revenue,
|
|
slices: revenue.slices.slice(0, 4)
|
|
}), null);
|
|
assert.equal(workflow.normalizeRecord("revenue-composition", {
|
|
...revenue,
|
|
slices: [{ label: "bad_label", percentageText: "50", percentage: 50 }, ...revenue.slices.slice(1)]
|
|
}), null);
|
|
assert.equal(workflow.normalizeRecord("revenue-composition", {
|
|
...revenue,
|
|
slices: [{ label: "A", percentageText: "50", percentage: 49 }, ...revenue.slices.slice(1)]
|
|
}), null);
|
|
assert.equal(workflow.normalizeRecord("growth-metrics", {
|
|
...record("growth-metrics"),
|
|
salesGrowth: [1, 2, Number.POSITIVE_INFINITY, 4]
|
|
}), null);
|
|
assert.equal(workflow.normalizeRecord("sales", {
|
|
...record("sales"),
|
|
quarters: quarters().slice(1)
|
|
}), null);
|
|
assert.equal(workflow.normalizeRecord("sales", {
|
|
...record("sales"),
|
|
stockName: "bad\nname"
|
|
}), null);
|
|
});
|
|
|
|
test("list requests normalize blank/search text and enforce exact screen bounds", () => {
|
|
assert.deepEqual(workflow.createListRequest("list-1", "sales", " "), {
|
|
requestId: "list-1",
|
|
screen: "sales",
|
|
query: ""
|
|
});
|
|
assert.deepEqual(listRequest(), {
|
|
requestId: "list-1",
|
|
screen: "sales",
|
|
query: "Alpha",
|
|
maximumResults: 25
|
|
});
|
|
assert.throws(() => workflow.createListRequest("bad id", "sales"), /request id/);
|
|
assert.throws(() => workflow.createListRequest("list", "unknown"), /screen/);
|
|
assert.throws(() => workflow.createListRequest("list", "sales", "", 1001), /limited/);
|
|
});
|
|
|
|
test("native list responses create trusted snapshots and reject duplicate or reordered names", () => {
|
|
const request = workflow.createListRequest("list-1", "sales", "Alpha", 25);
|
|
const payload = listPayload("sales", [record("sales", "Alpha"), record("sales", "Beta")]);
|
|
const normalized = workflow.normalizeListResponse(payload, request);
|
|
assert.ok(normalized);
|
|
assert.equal(normalized.items[0].rowVersion, version);
|
|
assert.ok(Object.isFrozen(normalized.items));
|
|
|
|
assert.equal(workflow.normalizeListResponse({
|
|
...payload,
|
|
items: [...payload.items].reverse()
|
|
}, request), null);
|
|
assert.equal(workflow.normalizeListResponse(listPayload("sales", [
|
|
record("sales", "Alpha"), record("sales", "ALPHA")
|
|
]), request), null);
|
|
assert.equal(workflow.normalizeListResponse({ ...payload, requestId: "stale" }, request), null);
|
|
assert.equal(workflow.normalizeListResponse({
|
|
...payload,
|
|
items: [{ ...payload.items[0], rowVersion: "bad" }, payload.items[1]]
|
|
}, request), null);
|
|
});
|
|
|
|
test("list errors are exact and correlated", () => {
|
|
const request = listRequest();
|
|
assert.deepEqual(workflow.normalizeListError({
|
|
requestId: "list-1",
|
|
screen: "sales",
|
|
query: "Alpha",
|
|
message: "Database unavailable"
|
|
}, request), {
|
|
requestId: "list-1",
|
|
screen: "sales",
|
|
query: "Alpha",
|
|
message: "Database unavailable"
|
|
});
|
|
assert.equal(workflow.normalizeListError({
|
|
requestId: "other",
|
|
screen: "sales",
|
|
query: "Alpha",
|
|
message: "Database unavailable"
|
|
}, request), null);
|
|
});
|
|
|
|
test("load responses are exact native refreshes and never trust a stale request", () => {
|
|
const snapshot = trustedSnapshot("sales");
|
|
const request = workflow.createLoadRequest("load-1", "sales", "Alpha");
|
|
assert.deepEqual(request, {
|
|
requestId: "load-1",
|
|
screen: "sales",
|
|
stockName: "Alpha"
|
|
});
|
|
const loaded = trustedLoad(snapshot);
|
|
assert.deepEqual(loaded.snapshot.record, snapshot.record);
|
|
assert.equal(workflow.normalizeLoadResponse({
|
|
requestId: "load-1",
|
|
screen: "sales",
|
|
retrievedAt: "2026-07-12T00:20:00+09:00",
|
|
snapshot: {
|
|
stockName: "Beta",
|
|
rowVersion: snapshot.rowVersion,
|
|
record: record("sales", "Beta")
|
|
}
|
|
}, request), null);
|
|
assert.deepEqual(workflow.normalizeLoadError({
|
|
requestId: "load-1",
|
|
screen: "sales",
|
|
stockName: "Alpha",
|
|
code: "NOT_FOUND",
|
|
message: "Missing",
|
|
retryable: false
|
|
}, request).code, "NOT_FOUND");
|
|
});
|
|
|
|
test("stock master normalization enforces provider identity and rejects duplicate rows", () => {
|
|
const normalized = workflow.normalizeStockSearchResponse(stockPayload());
|
|
assert.ok(normalized);
|
|
assert.equal(normalized.results[0].source, "oracle");
|
|
assert.equal(workflow.normalizeStockSearchResponse(stockPayload([{
|
|
market: "kospi", source: "mariaDb", name: "Alpha", code: "000001"
|
|
}])), null);
|
|
assert.equal(workflow.normalizeStockSearchResponse(stockPayload([
|
|
{ market: "kospi", source: "oracle", name: "Alpha", code: "000001" },
|
|
{ market: "kospi", source: "oracle", name: "Alpha", code: "000001" }
|
|
])), null);
|
|
});
|
|
|
|
test("verification rejects same-name market ambiguity and fabricated responses", () => {
|
|
const value = record("sales");
|
|
assert.equal(workflow.verifyStockForRecord(value, stockPayload(), "kospi", "000001"), null);
|
|
const response = workflow.normalizeStockSearchResponse(stockPayload([
|
|
{ market: "kospi", source: "oracle", name: "Alpha", code: "000001" },
|
|
{ market: "kosdaq", source: "oracle", name: "Alpha", code: "000002" }
|
|
]));
|
|
assert.ok(response);
|
|
assert.equal(workflow.verifyStockForRecord(value, response, "kospi", "000001"), null);
|
|
const one = workflow.normalizeStockSearchResponse(stockPayload());
|
|
assert.equal(workflow.verifyStockForRecord(value, one, "kosdaq", "000001"), null);
|
|
});
|
|
|
|
test("selection requests bind snapshot version and freshly verified stock to native result", () => {
|
|
const snapshot = trustedSnapshot("sales");
|
|
const stock = verifiedStock(snapshot.record);
|
|
const request = workflow.createSelectionRequest("selection-1", snapshot, stock);
|
|
assert.deepEqual(request, {
|
|
requestId: "selection-1",
|
|
screen: "sales",
|
|
stockName: "Alpha",
|
|
rowVersion: version,
|
|
stock: {
|
|
market: "kospi",
|
|
source: "oracle",
|
|
name: "Alpha",
|
|
code: "000001"
|
|
}
|
|
});
|
|
const response = trustedSelection(snapshot, stock);
|
|
assert.equal(response.builderKey, "s5080");
|
|
assert.equal(workflow.normalizeSelectionResponse({
|
|
...response,
|
|
rowVersion: "B".repeat(64)
|
|
}, request), null);
|
|
assert.deepEqual(workflow.normalizeSelectionError({
|
|
requestId: "selection-1",
|
|
screen: "sales",
|
|
stockName: "Alpha",
|
|
code: "STALE_ROW",
|
|
message: "Reload",
|
|
retryable: false
|
|
}, request).code, "STALE_ROW");
|
|
});
|
|
|
|
test("create requires a verified exact stock and sends no row version", () => {
|
|
const value = record("sales");
|
|
const stock = verifiedStock(value);
|
|
assert.deepEqual(workflow.createCreateRequest("create-1", value, stock), {
|
|
requestId: "create-1",
|
|
screen: "sales",
|
|
stock: {
|
|
market: "kospi",
|
|
source: "oracle",
|
|
name: "Alpha",
|
|
code: "000001"
|
|
},
|
|
record: value
|
|
});
|
|
assert.throws(() => workflow.createCreateRequest("create-1", value, {
|
|
market: "kospi", source: "oracle", name: "Alpha", code: "000001"
|
|
}), /verified stock/);
|
|
assert.throws(() => workflow.createCreateRequest(
|
|
"create-1",
|
|
record("sales", "Beta"),
|
|
stock), /verified stock/);
|
|
});
|
|
|
|
test("update and single delete require trusted snapshots and optimistic row version", () => {
|
|
const snapshot = trustedSnapshot("sales");
|
|
const replacement = record("sales");
|
|
assert.deepEqual(workflow.createUpdateRequest("update-1", snapshot, replacement), {
|
|
requestId: "update-1",
|
|
screen: "sales",
|
|
stockName: "Alpha",
|
|
expectedRowVersion: version,
|
|
record: replacement
|
|
});
|
|
assert.deepEqual(workflow.createDeleteRequest("delete-1", snapshot), {
|
|
requestId: "delete-1",
|
|
screen: "sales",
|
|
stockName: "Alpha",
|
|
expectedRowVersion: version
|
|
});
|
|
assert.throws(() => workflow.createUpdateRequest(
|
|
"update-1",
|
|
snapshot,
|
|
record("sales", "Beta")), /cannot rename/);
|
|
assert.throws(() => workflow.createDeleteRequest("delete-1", {
|
|
...snapshot
|
|
}), /native-normalized/);
|
|
});
|
|
|
|
test("delete-all requires the exact table-specific confirmation token", () => {
|
|
assert.deepEqual(workflow.createDeleteAllRequest(
|
|
"delete-all-1",
|
|
"sales",
|
|
"DELETE_ALL_INPUT_SELL"), {
|
|
requestId: "delete-all-1",
|
|
screen: "sales",
|
|
confirmationToken: "DELETE_ALL_INPUT_SELL"
|
|
});
|
|
assert.throws(() => workflow.createDeleteAllRequest(
|
|
"delete-all-1",
|
|
"sales",
|
|
"DELETE_ALL_INPUT_PROFIT"), /confirmation token/);
|
|
});
|
|
|
|
test("mutation results correlate and errors can never request automatic retry", () => {
|
|
const snapshot = trustedSnapshot("sales");
|
|
const request = workflow.createDeleteRequest("delete-1", snapshot);
|
|
assert.deepEqual(workflow.normalizeMutationResult({
|
|
requestId: "delete-1",
|
|
operationId: "operation-1",
|
|
operation: "delete-one",
|
|
screen: "sales",
|
|
stockName: "Alpha",
|
|
committedAt: "2026-07-12T00:12:00+09:00",
|
|
affectedRows: 1
|
|
}, request), {
|
|
requestId: "delete-1",
|
|
operationId: "operation-1",
|
|
operation: "delete-one",
|
|
screen: "sales",
|
|
stockName: "Alpha",
|
|
committedAt: "2026-07-12T00:12:00+09:00",
|
|
affectedRows: 1
|
|
});
|
|
assert.deepEqual(workflow.normalizeMutationError({
|
|
requestId: "delete-1",
|
|
operation: "delete-one",
|
|
screen: "sales",
|
|
stockName: "Alpha",
|
|
code: "OUTCOME_UNKNOWN",
|
|
message: "Do not retry automatically",
|
|
retryable: false,
|
|
outcomeUnknown: true
|
|
}, request).outcomeUnknown, true);
|
|
assert.equal(workflow.normalizeMutationResult({
|
|
requestId: "delete-1",
|
|
operationId: "operation-1",
|
|
operation: "update",
|
|
screen: "sales",
|
|
stockName: "Alpha",
|
|
committedAt: "2026-07-12T00:12:00+09:00",
|
|
affectedRows: 1
|
|
}, request), null);
|
|
assert.equal(workflow.normalizeMutationError({
|
|
requestId: "delete-1",
|
|
operation: "delete-one",
|
|
screen: "sales",
|
|
stockName: "Alpha",
|
|
code: "OUTCOME_UNKNOWN",
|
|
message: "retry",
|
|
retryable: true,
|
|
outcomeUnknown: true
|
|
}, request), null);
|
|
});
|
|
|
|
test("only trusted DB snapshots and verified master rows create the four exact one-page cuts", () => {
|
|
const expectations = [
|
|
["revenue-composition", "s5076", "5076", "REVENUE_COMPOSITION"],
|
|
["growth-metrics", "s5079", "5079", "GROWTH_METRICS"],
|
|
["sales", "s5080", "5080", "SALES"],
|
|
["operating-profit", "s5081", "5081", "OPERATING_PROFIT"]
|
|
];
|
|
for (const [screen, builderKey, code, graphicType] of expectations) {
|
|
const snapshot = trustedSnapshot(screen);
|
|
const stock = verifiedStock(snapshot.record);
|
|
assert.deepEqual(workflow.buildSelection(snapshot, stock), {
|
|
builderKey,
|
|
code,
|
|
aliases: [code],
|
|
selection: {
|
|
groupCode: "KOSPI",
|
|
subject: "Alpha",
|
|
graphicType,
|
|
subtype: "",
|
|
dataCode: ""
|
|
},
|
|
currentPage: 1,
|
|
pageCount: 1
|
|
});
|
|
}
|
|
const rawSnapshot = listPayload("sales").items[0];
|
|
const stock = verifiedStock(record("sales"));
|
|
assert.equal(workflow.buildSelection(rawSnapshot, stock), null);
|
|
});
|
|
|
|
test("playlist entry has the complete standard operator metadata and native trust", () => {
|
|
const snapshot = trustedSnapshot("operating-profit");
|
|
const stock = verifiedStock(snapshot.record);
|
|
const selection = trustedSelection(snapshot, stock);
|
|
const entry = workflow.createPlaylistEntry(snapshot, stock, selection, {
|
|
id: "profit-1",
|
|
fadeDuration: 8
|
|
});
|
|
assert.deepEqual(entry, {
|
|
id: "profit-1",
|
|
builderKey: "s5081",
|
|
code: "5081",
|
|
aliases: ["5081"],
|
|
title: "Alpha",
|
|
detail: "영업이익",
|
|
category: "manual-financial",
|
|
market: "kospi",
|
|
stockName: "Alpha",
|
|
stockCode: "000001",
|
|
isNxt: false,
|
|
selection: {
|
|
groupCode: "KOSPI",
|
|
subject: "Alpha",
|
|
graphicType: "OPERATING_PROFIT",
|
|
subtype: "",
|
|
dataCode: ""
|
|
},
|
|
enabled: true,
|
|
fadeDuration: 8,
|
|
graphicType: "OPERATING_PROFIT",
|
|
subtype: "",
|
|
currentPage: 1,
|
|
pageCount: 1,
|
|
operator: {
|
|
source: "manual-financial-workflow",
|
|
schemaVersion: 1,
|
|
screen: "operating-profit",
|
|
snapshot: {
|
|
screen: "operating-profit",
|
|
stockName: "Alpha",
|
|
rowVersion: version,
|
|
record: snapshot.record
|
|
},
|
|
verifiedStock: {
|
|
market: "kospi",
|
|
source: "oracle",
|
|
name: "Alpha",
|
|
code: "000001"
|
|
}
|
|
}
|
|
});
|
|
assert.equal(workflow.isPlaylistEntryPlayable(entry), true);
|
|
assert.equal(workflow.isPlaylistEntryPlayable(JSON.parse(JSON.stringify(entry))), false);
|
|
assert.throws(() => workflow.createPlaylistEntry(snapshot, stock, {
|
|
...selection
|
|
}, { id: "forged" }), /native-confirmed/);
|
|
});
|
|
|
|
test("stored entries restore only as non-playable refresh-required descriptors", () => {
|
|
const snapshot = trustedSnapshot("sales");
|
|
const stock = verifiedStock(snapshot.record);
|
|
const selection = trustedSelection(snapshot, stock);
|
|
const live = workflow.createPlaylistEntry(snapshot, stock, selection, {
|
|
id: "sales-restore",
|
|
fadeDuration: 7,
|
|
enabled: false
|
|
});
|
|
const stored = JSON.parse(JSON.stringify(live));
|
|
const pending = workflow.restorePlaylistEntry(stored);
|
|
assert.ok(pending);
|
|
assert.equal(pending.status, "refresh-required");
|
|
assert.equal(pending.playable, false);
|
|
assert.equal(workflow.isPlaylistEntryPlayable(pending.entry), false);
|
|
assert.deepEqual(workflow.createRestoreLoadRequest("restore-load", pending), {
|
|
requestId: "restore-load",
|
|
screen: "sales",
|
|
stockName: "Alpha"
|
|
});
|
|
|
|
assert.equal(workflow.restorePlaylistEntry({
|
|
...stored,
|
|
selection: { ...stored.selection, dataCode: "tampered" }
|
|
}), null);
|
|
assert.equal(workflow.restorePlaylistEntry({
|
|
...stored,
|
|
operator: { ...stored.operator, schemaVersion: 2 }
|
|
}), null);
|
|
});
|
|
|
|
test("native load, fresh stock verification and native selection materialize a stored entry", () => {
|
|
const originalSnapshot = trustedSnapshot("sales");
|
|
const originalStock = verifiedStock(originalSnapshot.record);
|
|
const originalSelection = trustedSelection(originalSnapshot, originalStock);
|
|
const stored = JSON.parse(JSON.stringify(workflow.createPlaylistEntry(
|
|
originalSnapshot,
|
|
originalStock,
|
|
originalSelection,
|
|
{ id: "sales-restore", fadeDuration: 7, enabled: false })));
|
|
const pending = workflow.restorePlaylistEntry(stored);
|
|
|
|
const loadRequest = workflow.createRestoreLoadRequest("restore-load", pending);
|
|
const loadResponse = workflow.normalizeLoadResponse({
|
|
requestId: "restore-load",
|
|
screen: "sales",
|
|
retrievedAt: "2026-07-12T00:20:00+09:00",
|
|
snapshot: {
|
|
stockName: stored.operator.snapshot.stockName,
|
|
rowVersion: stored.operator.snapshot.rowVersion,
|
|
record: stored.operator.snapshot.record
|
|
}
|
|
}, loadRequest);
|
|
assert.ok(loadResponse);
|
|
const freshStockResponse = workflow.normalizeStockSearchResponse(stockPayload());
|
|
const freshStock = workflow.verifyStockForRecord(
|
|
loadResponse.snapshot.record,
|
|
freshStockResponse,
|
|
pending.verifiedStock.market,
|
|
pending.verifiedStock.code);
|
|
assert.ok(freshStock);
|
|
const selectionRequest = workflow.createRestoreSelectionRequest(
|
|
"restore-selection",
|
|
pending,
|
|
loadResponse,
|
|
freshStock);
|
|
const profile = workflow.getScreenDefinition("sales");
|
|
const selectionResponse = workflow.normalizeSelectionResponse({
|
|
requestId: "restore-selection",
|
|
screen: "sales",
|
|
stockName: "Alpha",
|
|
rowVersion: version,
|
|
builderKey: profile.builderKey,
|
|
code: profile.code,
|
|
aliases: [profile.code],
|
|
selection: {
|
|
groupCode: "KOSPI",
|
|
subject: "Alpha",
|
|
graphicType: profile.graphicType,
|
|
subtype: "",
|
|
dataCode: ""
|
|
},
|
|
currentPage: 1,
|
|
totalPages: 1
|
|
}, selectionRequest);
|
|
const restored = workflow.materializeRestoredPlaylistEntry(
|
|
pending,
|
|
loadResponse,
|
|
freshStock,
|
|
selectionResponse);
|
|
assert.deepEqual(restored, stored);
|
|
assert.equal(workflow.isPlaylistEntryPlayable(restored), true);
|
|
});
|
|
|
|
test("row-version or record drift keeps a stored entry permanently unplayable", () => {
|
|
const snapshot = trustedSnapshot("sales");
|
|
const stock = verifiedStock(snapshot.record);
|
|
const selection = trustedSelection(snapshot, stock);
|
|
const stored = JSON.parse(JSON.stringify(workflow.createPlaylistEntry(
|
|
snapshot,
|
|
stock,
|
|
selection,
|
|
{ id: "stale-entry" })));
|
|
const pending = workflow.restorePlaylistEntry(stored);
|
|
const request = workflow.createRestoreLoadRequest("restore-load", pending);
|
|
const changed = workflow.normalizeLoadResponse({
|
|
requestId: "restore-load",
|
|
screen: "sales",
|
|
retrievedAt: "2026-07-12T00:20:00+09:00",
|
|
snapshot: {
|
|
stockName: "Alpha",
|
|
rowVersion: "B".repeat(64),
|
|
record: record("sales")
|
|
}
|
|
}, request);
|
|
const freshStockResponse = workflow.normalizeStockSearchResponse(stockPayload());
|
|
const freshStock = workflow.verifyStockForRecord(
|
|
changed.snapshot.record,
|
|
freshStockResponse,
|
|
"kospi",
|
|
"000001");
|
|
assert.throws(() => workflow.createRestoreSelectionRequest(
|
|
"restore-selection",
|
|
pending,
|
|
changed,
|
|
freshStock), /did not reproduce/);
|
|
assert.equal(workflow.materializeRestoredPlaylistEntry(
|
|
pending,
|
|
changed,
|
|
freshStock,
|
|
selection), null);
|
|
assert.equal(workflow.isPlaylistEntryPlayable(stored), false);
|
|
});
|