"use strict"; const test = require("node:test"); const assert = require("node:assert/strict"); const workflow = require("../../Web/named-playlist-workflow.js"); const retrievedAt = "2026-07-11T09:30:00+09:00"; const selection = Object.freeze({ groupCode: "KOSPI", subject: "Samsung Electronics", graphicType: "ONE_COLUMN", subtype: "CURRENT", dataCode: "005930" }); function trustedEntry(overrides = {}) { return { id: "entry-1", builderKey: "s5001", code: "5001", enabled: true, selection, operator: { source: "trusted-test", schemaVersion: 1 }, ...overrides }; } function nativeRow(overrides = {}) { return { itemIndex: 0, enabled: true, groupCode: "KOSPI", subject: "Samsung Electronics", graphicType: "ONE_COLUMN", subtype: "CURRENT", pageText: "1/1", dataCode: "005930", ...overrides }; } function loadResponse(rows = [nativeRow()], overrides = {}) { return { requestId: "load-1", definition: { programCode: "00000001", title: "Morning" }, retrievedAt, totalRowCount: rows.length, canMutate: true, writeQuarantined: false, rows, ...overrides }; } test("named-playlist bridge contract is closed and bounded", () => { assert.deepEqual(workflow.bridgeContract, { requests: { list: "request-named-playlist-list", load: "request-named-playlist-load", create: "create-named-playlist", replace: "replace-named-playlist", delete: "delete-named-playlist" }, responses: { list: "named-playlist-list-results", load: "named-playlist-load-results", mutation: "named-playlist-mutation-results", error: "named-playlist-error" }, schemaVersion: 1, maximumDefinitions: 1000, maximumItems: 1000, maximumPageCount: 20, maximumStoredPageCount: 9999 }); assert.deepEqual(workflow.operations, ["list", "load", "create", "replace", "delete"]); }); test("list, load, create and delete request payloads require canonical identities", () => { assert.deepEqual(workflow.createListRequest("list-1"), { requestId: "list-1", maximumResults: 200 }); assert.deepEqual(workflow.createListRequest("list-2", 1000), { requestId: "list-2", maximumResults: 1000 }); assert.deepEqual(workflow.createLoadRequest("load-1", "00000001"), { requestId: "load-1", programCode: "00000001" }); assert.deepEqual(workflow.createDefinitionRequest("create-1", "00000001", "Morning"), { requestId: "create-1", programCode: "00000001", title: "Morning" }); assert.deepEqual(workflow.createDeleteRequest("delete-1", "00000001"), { requestId: "delete-1", programCode: "00000001" }); assert.throws(() => workflow.createListRequest("bad id"), /safe/i); assert.throws(() => workflow.createListRequest("list", 1001), /1 through 1000/i); assert.throws(() => workflow.createLoadRequest("load", "1"), /eight-digit/i); assert.throws(() => workflow.createDefinitionRequest("create", "00000001", " Morning"), /canonical/i); }); test("trusted entries serialize to the exact original seven LIST_TEXT fields", () => { const entry = trustedEntry(); assert.deepEqual(workflow.toLegacySevenFields(entry, "2/4"), [ "1", "KOSPI", "Samsung Electronics", "ONE_COLUMN", "CURRENT", "2/4", "005930" ]); assert.equal( workflow.legacyListText(workflow.toLegacySevenFields(entry, "2/4")), "1^KOSPI^Samsung Electronics^ONE_COLUMN^CURRENT^2/4^005930"); const request = workflow.createReplaceRequest( "replace-1", "00000001", [entry, trustedEntry({ id: "entry-2", enabled: false })], { isTrustedEntry: () => true, pageTextForEntry: (_, index) => index === 0 ? "2/4" : "1/0" }); assert.deepEqual(request.items, [ { itemIndex: 0, enabled: true, groupCode: "KOSPI", subject: "Samsung Electronics", graphicType: "ONE_COLUMN", subtype: "CURRENT", pageText: "2/4", dataCode: "005930" }, { itemIndex: 1, enabled: false, groupCode: "KOSPI", subject: "Samsung Electronics", graphicType: "ONE_COLUMN", subtype: "CURRENT", pageText: "1/0", dataCode: "005930" } ]); }); test("legacy LIST_TEXT accepts a bounded 100-name VI subject beyond 256 characters", () => { const names = Array.from( { length: 100 }, (_, index) => `종목${String(index).padStart(3, "0")}`).join(","); assert.ok(names.length > 256); const fields = ["1", "", names, "5단 표그래프", "VI 발동(수동)", "1/20", ""]; const serialized = workflow.legacyListText(fields); assert.ok(serialized.length <= 4000); const response = workflow.normalizeLoadResponse(loadResponse([nativeRow({ groupCode: "", subject: names, graphicType: "5단 표그래프", subtype: "VI 발동(수동)", pageText: "1/20", dataCode: "" })])); assert.equal(response.rawRows[0].subject, names); assert.throws(() => workflow.legacyListText([ "1", "", "가".repeat(3990), "5단 표그래프", "VI 발동(수동)", "1/20", "" ]), /too long/u); }); test("database replacement requires trusted verification and enforces the 1000-row bound", () => { assert.throws(() => workflow.createReplaceRequest( "replace", "00000001", [trustedEntry()]), /trusted-entry verifier/i); assert.throws(() => workflow.createReplaceRequest( "replace", "00000001", [trustedEntry()], { isTrustedEntry: () => false }), /trusted restoration/i); const entries = Array.from({ length: 1000 }, (_, index) => trustedEntry({ id: `entry-${index}` })); const bounded = workflow.createReplaceRequest( "replace", "00000001", entries, { isTrustedEntry: () => true }); assert.equal(bounded.items.length, 1000); assert.throws(() => workflow.createReplaceRequest( "replace", "00000001", [...entries, trustedEntry({ id: "overflow" })], { isTrustedEntry: () => true }), /at most 1000/i); }); test("unsafe selection delimiters, controls and noncanonical pages never reach the bridge", () => { assert.throws(() => workflow.toLegacySevenFields(trustedEntry({ selection: { ...selection, subject: "Alpha^Beta" } })), /cannot be stored safely/i); assert.throws(() => workflow.toLegacySevenFields(trustedEntry({ selection: { ...selection, subject: "Alpha\nBeta" } })), /cannot be stored safely/i); assert.throws(() => workflow.toLegacySevenFields(trustedEntry(), "01/02"), /page text/i); assert.throws(() => workflow.toLegacySevenFields(trustedEntry(), "0/0"), /page text/i); assert.throws(() => workflow.toLegacySevenFields(trustedEntry(), "1/10000"), /page text/i); assert.throws(() => workflow.legacyListText([ "yes", "KOSPI", "Alpha", "ONE_COLUMN", "CURRENT", "1/1", "000001" ]), /seven safe/i); }); test("stored page parsing preserves historical values but marks the current 20-page boundary", () => { assert.deepEqual(workflow.parseStoredPageText("4/20"), { pageText: "4/20", currentPage: 4, totalPages: 20, isWithinPlayoutBounds: true, historicalPageOutOfBounds: false, pageRecalculationRequired: true }); assert.deepEqual(workflow.parseStoredPageText("1/0"), { pageText: "1/0", currentPage: 1, totalPages: 0, isWithinPlayoutBounds: false, historicalPageOutOfBounds: true, pageRecalculationRequired: true }); assert.deepEqual(workflow.parseStoredPageText("1/24"), { pageText: "1/24", currentPage: 1, totalPages: 24, isWithinPlayoutBounds: false, historicalPageOutOfBounds: true, pageRecalculationRequired: true }); for (const value of ["", "01/1", "1/01", "2/1", "2/0", "1/10000", "text"]) { assert.equal(workflow.parseStoredPageText(value), null); } }); test("list responses are exact, duplicate-safe and expose permanent write quarantine", () => { const response = { requestId: "list-1", retrievedAt, totalRowCount: 2, truncated: false, suggestedProgramCode: "00000003", canMutate: true, writeQuarantined: false, definitions: [ { programCode: "00000001", title: "Morning" }, { programCode: "00000002", title: "Noon" } ] }; assert.deepEqual(workflow.normalizeListResponse(response), response); assert.equal(workflow.normalizeListResponse({ ...response, extra: true }), null); assert.equal(workflow.normalizeListResponse({ ...response, definitions: [response.definitions[0], response.definitions[0]] }), null); assert.equal(workflow.normalizeListResponse({ ...response, canMutate: true, writeQuarantined: true }), null); const quarantined = workflow.normalizeListResponse({ ...response, canMutate: false, writeQuarantined: true }); assert.equal(workflow.isWriteBlocked(quarantined), true); }); test("load normalization preserves every raw field before trusted restoration", () => { const normalized = workflow.normalizeLoadResponse(loadResponse([ nativeRow({ pageText: "1/24" }), nativeRow({ itemIndex: 1, enabled: false, subject: "SK hynix", dataCode: "000660", pageText: "" }) ])); assert.ok(normalized); assert.equal(normalized.rawRows.length, 2); assert.deepEqual(normalized.rawRows[0].legacyFields, [ "1", "KOSPI", "Samsung Electronics", "ONE_COLUMN", "CURRENT", "1/24", "005930" ]); assert.equal(normalized.rawRows[0].listText, "1^KOSPI^Samsung Electronics^ONE_COLUMN^CURRENT^1/24^005930"); assert.equal(normalized.rawRows[0].requiresTrustedRestore, true); assert.equal(normalized.rawRows[0].pageRecalculationRequired, true); assert.equal(normalized.rawRows[0].historicalPageOutOfBounds, true); assert.equal(normalized.rawRows[1].requiresTrustedRestore, true); assert.equal(normalized.rawRows[1].pageRecalculationRequired, false); assert.match(workflow.pageStatusLabel(normalized.rawRows[0]), /20-page maximum/i); }); test("load responses reject extra fields, reordered indexes and malformed historical pages", () => { assert.equal(workflow.normalizeLoadResponse({ ...loadResponse(), extra: true }), null); assert.equal(workflow.normalizeLoadResponse(loadResponse([nativeRow({ itemIndex: 1 })])), null); assert.equal(workflow.normalizeLoadResponse(loadResponse([nativeRow({ pageText: "01/1" })])), null); assert.equal(workflow.normalizeLoadResponse(loadResponse([nativeRow({ subject: " Alpha" })])), null); assert.equal(workflow.normalizeLoadResponse(loadResponse([], { totalRowCount: 1 })), null); }); test("raw database rows cannot become playable until a trusted resolver reconstructs them", () => { const load = workflow.normalizeLoadResponse(loadResponse()); const unresolved = workflow.restoreRawRows(load, () => null); assert.equal(unresolved.readyForPrepare, false); assert.deepEqual(unresolved.blockers.map(value => value.reason), [ "trusted-restore-required", "page-recalculation-required" ]); assert.throws(() => workflow.materializeTrustedPlaylist(unresolved), /trusted restoration/i); assert.equal(unresolved.rows[0].rawRow, load.rawRows[0]); }); test("trusted resolver output must reproduce enabled state and all five selection fields", () => { const load = workflow.normalizeLoadResponse(loadResponse()); assert.equal(workflow.restoreRawRows(load, () => trustedEntry()).rows[0].trusted, true); assert.equal(workflow.restoreRawRows(load, () => trustedEntry({ enabled: false })).rows[0].trusted, false); assert.equal(workflow.restoreRawRows(load, () => trustedEntry({ selection: { ...selection, subject: "Tampered" } })).rows[0].trusted, false); assert.equal(workflow.restoreRawRows(load, () => trustedEntry({ code: "bad" })).rows[0].trusted, false); assert.equal(workflow.restoreRawRows(load, () => trustedEntry({ builderKey: "5001" })).rows[0].trusted, false); }); test("fresh page plans are mandatory before PREPARE and cannot exceed 20 pages", () => { const load = workflow.normalizeLoadResponse(loadResponse([nativeRow({ pageText: "1/24" })])); const restored = workflow.restoreRawRows(load, () => trustedEntry()); assert.equal(restored.readyForPrepare, false); assert.throws(() => workflow.markPageRecalculated(restored, 0, { itemCount: 101, pageSize: 5, pageCount: 21 }), /exceeds 20 pages/i); const recalculated = workflow.markPageRecalculated(restored, 0, { itemCount: 100, pageSize: 5, pageCount: 20 }); assert.equal(recalculated.readyForPrepare, true); assert.equal(workflow.canPrepareRestoredRows(recalculated), true); assert.deepEqual(recalculated.rows[0].recalculatedPagePlan, { itemCount: 100, pageSize: 5, pageCount: 20 }); assert.equal(recalculated.rows[0].rawRow.pageText, "1/24"); assert.deepEqual(workflow.materializeTrustedPlaylist(recalculated), [trustedEntry()]); }); test("paged trusted rows create a strict preflight request without using stored page text", () => { function prepare(pageText) { const load = workflow.normalizeLoadResponse(loadResponse([nativeRow({ pageText })])); const restored = workflow.restoreRawRows(load, () => trustedEntry({ builderKey: "s5074", code: "5074", fadeDuration: 6 })); return workflow.preparePagePreflight(restored, entry => entry.builderKey === "s5074"); } const request = workflow.createPagePlanRequest("page-plan-1", prepare("1/24")); const requestFromDifferentHistoricalPage = workflow.createPagePlanRequest( "page-plan-1", prepare("4/20")); assert.deepEqual(request, { requestId: "page-plan-1", entries: [{ id: "entry-1", code: "5074", enabled: true, fadeDuration: 6, selection }] }); assert.deepEqual(requestFromDifferentHistoricalPage, request); assert.equal(Object.hasOwn(request.entries[0], "pageText"), false); }); test("correlated capped page plans enable PREPARE and preserve explicit truncation", () => { const load = workflow.normalizeLoadResponse(loadResponse([nativeRow({ pageText: "1/24" })])); let restored = workflow.restoreRawRows(load, () => trustedEntry({ builderKey: "s5074", code: "5074", fadeDuration: 6 })); restored = workflow.preparePagePreflight(restored, () => true); const request = workflow.createPagePlanRequest("page-plan-2", restored); const rawResponse = { requestId: "page-plan-2", calculatedAt: retrievedAt, plans: [{ entryId: "entry-1", itemCount: 101, pageSize: 5, pageCount: 20, accessibleItemCount: 100, isTruncated: true, builderKey: "s5074" }] }; const response = workflow.normalizePagePlanResponse(rawResponse, request); assert.ok(response); restored = workflow.applyPagePlanResponse(restored, response); assert.equal(restored.readyForPrepare, true); assert.equal(restored.rows[0].pageRecalculated, true); assert.deepEqual(restored.rows[0].recalculatedPagePlan, rawResponse.plans[0]); assert.equal(workflow.normalizePagePlanResponse({ ...rawResponse, requestId: "stale-request" }, request), null); assert.equal(workflow.normalizePagePlanResponse({ ...rawResponse, plans: [{ ...rawResponse.plans[0], accessibleItemCount: 101 }] }, request), null); }); test("zero-row page preflight is complete but explicitly unavailable for PREPARE", () => { const load = workflow.normalizeLoadResponse(loadResponse([nativeRow({ pageText: "" })])); let restored = workflow.restoreRawRows(load, () => trustedEntry({ builderKey: "s5077", code: "5077", fadeDuration: 6 })); restored = workflow.preparePagePreflight(restored, () => true); const request = workflow.createPagePlanRequest("page-plan-zero", restored); const response = workflow.normalizePagePlanResponse({ requestId: "page-plan-zero", calculatedAt: retrievedAt, plans: [{ entryId: "entry-1", itemCount: 0, pageSize: 6, pageCount: 0, accessibleItemCount: 0, isTruncated: false, builderKey: "s5077" }] }, request); restored = workflow.applyPagePlanResponse(restored, response); assert.equal(restored.readyForPrepare, false); assert.equal(restored.rows[0].pagePreflightCompleted, true); assert.equal(restored.rows[0].pageRecalculated, false); assert.deepEqual(restored.blockers.map(value => value.reason), ["page-result-empty"]); }); test("nonpaged rows become ready without a synthetic page-plan result", () => { const load = workflow.normalizeLoadResponse(loadResponse([nativeRow({ pageText: "1/24" })])); const restored = workflow.preparePagePreflight( workflow.restoreRawRows(load, () => trustedEntry()), () => false); assert.equal(restored.readyForPrepare, true); assert.equal(workflow.createPagePlanRequest("no-page-plan", restored), null); assert.equal(restored.rows[0].recalculatedPagePlan, null); }); test("rows without stored page text still require trust but no artificial page plan", () => { const load = workflow.normalizeLoadResponse(loadResponse([nativeRow({ pageText: "" })])); const restored = workflow.restoreRawRows(load, () => trustedEntry()); assert.equal(restored.readyForPrepare, true); assert.deepEqual(restored.blockers, []); assert.equal(restored.rows[0].pageRecalculated, true); }); test("mutation responses are strict and retain quarantine state", () => { const response = { requestId: "replace-1", operation: "replace", programCode: "00000001", completedAt: retrievedAt, writeQuarantined: false }; assert.deepEqual(workflow.normalizeMutationResponse(response), response); assert.equal(workflow.normalizeMutationResponse({ ...response, operation: "load" }), null); assert.equal(workflow.normalizeMutationResponse({ ...response, extra: true }), null); }); test("OutcomeUnknown errors permanently block writes and can never be retryable", () => { const error = { requestId: "replace-1", operation: "replace", programCode: "00000001", code: "OUTCOME_UNKNOWN", message: "Restart before another write.", retryable: false, outcomeUnknown: true, writeQuarantined: true }; const normalized = workflow.normalizeError(error); assert.deepEqual(normalized, error); assert.equal(workflow.isWriteBlocked(normalized), true); assert.equal(workflow.normalizeError({ ...error, retryable: true }), null); assert.equal(workflow.normalizeError({ ...error, writeQuarantined: false }), null); assert.equal(workflow.normalizeError({ ...error, extra: true }), null); });