94 lines
5.2 KiB
JavaScript
94 lines
5.2 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 workflow = require("../../Web/named-playlist-workflow.js");
|
|
|
|
const repositoryRoot = path.resolve(__dirname, "..", "..");
|
|
const mainWindow = fs.readFileSync(path.join(repositoryRoot, "MainWindow.xaml.cs"), "utf8");
|
|
const namedBridge = fs.readFileSync(path.join(repositoryRoot, "MainWindow.NamedPlaylists.cs"), "utf8");
|
|
const pagePlanBridge = fs.readFileSync(path.join(repositoryRoot, "MainWindow.PlaylistPagePlans.cs"), "utf8");
|
|
const appScript = fs.readFileSync(path.join(repositoryRoot, "Web", "app.js"), "utf8");
|
|
|
|
test("native switch and JavaScript contract use the same five requests and four responses", () => {
|
|
for (const requestType of Object.values(workflow.bridgeContract.requests)) {
|
|
assert.match(mainWindow, new RegExp(`case "${requestType}"`));
|
|
}
|
|
for (const responseType of Object.values(workflow.bridgeContract.responses)) {
|
|
assert.match(namedBridge, new RegExp(`"${responseType}"`));
|
|
}
|
|
assert.match(mainWindow, /new LegacyNamedPlaylistPersistenceService\(/);
|
|
assert.match(mainWindow, /new OracleNamedPlaylistMutationExecutor\(/);
|
|
});
|
|
|
|
test("named reads use latest-request cancellation and the shared playout-priority database gate", () => {
|
|
assert.match(namedBridge,
|
|
/Interlocked\.Exchange\(\s*ref _namedPlaylistReadCancellation,\s*requestCancellation\)/s);
|
|
assert.match(namedBridge, /CancelRequest\(previousCancellation\)/);
|
|
assert.ok((namedBridge.match(/await _databaseActivityGate\.WaitAsync\(requestToken\)/g) || []).length >= 2);
|
|
assert.ok((namedBridge.match(/Volatile\.Read\(ref _playoutCommandInFlight\) != 0/g) || []).length >= 4);
|
|
assert.match(mainWindow, /CancelRequest\(Volatile\.Read\(ref _namedPlaylistReadCancellation\)\)/);
|
|
assert.match(mainWindow, /CancelRequest\(Volatile\.Read\(ref _namedPlaylistWriteCancellation\)\)/);
|
|
});
|
|
|
|
test("writes are single-flight, never superseded, and permanently quarantine OutcomeUnknown", () => {
|
|
assert.match(namedBridge, /_namedPlaylistWriteGate\.WaitAsync\(0\)/);
|
|
assert.match(namedBridge, /catch \(NamedPlaylistMutationException exception\)/);
|
|
assert.match(namedBridge, /if \(exception\.OutcomeUnknown\)/);
|
|
assert.match(namedBridge,
|
|
/Interlocked\.CompareExchange\(ref _namedPlaylistWriteQuarantined, 1, 0\)/);
|
|
assert.match(namedBridge, /IsNamedPlaylistWriteQuarantined\(\)/);
|
|
assert.match(namedBridge, /do not retry|was not retried/i);
|
|
assert.doesNotMatch(namedBridge, /for\s*\([^)]*retry|while\s*\([^)]*retry/i);
|
|
});
|
|
|
|
test("browser correlation loss cancels active requests and latches an in-flight write", () => {
|
|
assert.match(mainWindow,
|
|
/private void QuarantineIfBrowserCorrelationCanBeLost\(\)\s*\{\s*InvalidateNamedPlaylistBrowserRequests\(\)/s);
|
|
assert.match(namedBridge, /Interlocked\.Increment\(ref _namedPlaylistBrowserGeneration\)/);
|
|
assert.match(namedBridge, /Volatile\.Read\(ref _namedPlaylistWriteInFlight\) != 0/);
|
|
assert.match(namedBridge,
|
|
/Interlocked\.Exchange\(\s*ref _namedPlaylistWriteCancellation,\s*null\)/s);
|
|
});
|
|
|
|
test("native replacement accepts only the exact eight raw-row properties and 1000 rows", () => {
|
|
for (const property of [
|
|
"itemIndex", "enabled", "groupCode", "subject",
|
|
"graphicType", "subtype", "pageText", "dataCode"
|
|
]) {
|
|
assert.match(namedBridge, new RegExp(`"${property}"`));
|
|
}
|
|
assert.match(namedBridge,
|
|
/itemsElement\.GetArrayLength\(\) > LegacyNamedPlaylistPersistenceService\.MaximumItems/);
|
|
assert.match(namedBridge, /itemIndex != expectedIndex/);
|
|
assert.match(namedBridge, /value\.Contains\('\^'\)/);
|
|
assert.match(namedBridge,
|
|
/return string\.Equals\(page\.ToString\(\), value, StringComparison\.Ordinal\)/);
|
|
});
|
|
|
|
test("page preflight uses one correlated read-only bridge with playout priority", () => {
|
|
assert.match(mainWindow, /case "request-playlist-page-plans"/);
|
|
assert.match(mainWindow, /CancelRequest\(Volatile\.Read\(ref _playlistPagePlanCancellation\)\)/);
|
|
assert.match(pagePlanBridge,
|
|
/Interlocked\.Exchange\(\s*ref _playlistPagePlanCancellation,\s*requestCancellation\)/s);
|
|
assert.match(pagePlanBridge, /await _databaseActivityGate\.WaitAsync\(requestToken\)/);
|
|
assert.ok((pagePlanBridge.match(/Volatile\.Read\(ref _playoutCommandInFlight\) != 0/g) || []).length >= 2);
|
|
assert.match(pagePlanBridge, /PreflightPagePlansAsync\(/);
|
|
assert.match(pagePlanBridge, /PostMessage\("playlist-page-plans-results"/);
|
|
assert.match(pagePlanBridge, /PostMessage\("playlist-page-plans-error"/);
|
|
assert.doesNotMatch(pagePlanBridge, /PrepareAsync\(|PlayAsync\(|ClearAsync\(|UnloadAsync\(/);
|
|
});
|
|
|
|
test("browser page-plan handling requires exact correlation and exposes explicit retry", () => {
|
|
assert.match(appScript, /normalizePagePlanResponse\(payload, pending\.request\)/);
|
|
assert.match(appScript, /payload\?\.requestId !== pending\.requestId/);
|
|
assert.match(appScript, /applyPagePlanResponse\(/);
|
|
assert.match(appScript, /page-result-empty/);
|
|
assert.match(appScript, /namedPlaylistPageRetryButton/);
|
|
assert.match(appScript, /case "playlist-page-plans-results"/);
|
|
assert.match(appScript, /case "playlist-page-plans-error"/);
|
|
assert.doesNotMatch(appScript, /markPageRecalculated\(/);
|
|
});
|