Prepare 4.0.5 Firefox update and Shorts fixes

Add Firefox data consent, repair contributor sign-in, and introduce optional Hide clutter buttons. Harden video navigation, voting, and action layouts with shared browser regression coverage and reproducible release packaging.
This commit is contained in:
Anarios
2026-09-06 12:51:12 +02:00
parent 3657de9b2c
commit e500088e65
196 changed files with 39706 additions and 2694 deletions
@@ -0,0 +1,76 @@
const { assertCurrentLiveArtifact } = require("../e2e/live/live-artifact-readiness");
const BUILD_ID = "0123456789abcdef0123456789abcdef";
function verifiers() {
return {
verifyExtensionBuildReceipt: jest.fn(() => ({ inputHash: "extension-inputs" })),
verifyExtensionJavaScript: jest.fn(() => ({ version: "4.0.5" })),
verifyUserscript: jest.fn(() => ({ inputHash: "userscript-inputs" })),
};
}
test("an extension live run verifies current source inputs and the generated browser bundles", () => {
const checks = verifiers();
expect(assertCurrentLiveArtifact("extension", BUILD_ID, checks)).toEqual({
artifact: { version: "4.0.5" },
buildId: BUILD_ID,
receipt: { inputHash: "extension-inputs" },
runtime: "extension",
});
expect(checks.verifyExtensionBuildReceipt).toHaveBeenCalledTimes(1);
expect(checks.verifyExtensionJavaScript).toHaveBeenCalledTimes(1);
expect(checks.verifyUserscript).not.toHaveBeenCalled();
});
test("a userscript live run verifies the exact live receipt and build ID", () => {
const checks = verifiers();
expect(assertCurrentLiveArtifact("userscript", BUILD_ID, checks)).toEqual({
buildId: BUILD_ID,
receipt: { inputHash: "userscript-inputs" },
runtime: "userscript",
});
expect(checks.verifyUserscript).toHaveBeenCalledWith({ expectedBuildId: BUILD_ID, liveTestBuild: true });
expect(checks.verifyExtensionBuildReceipt).not.toHaveBeenCalled();
expect(checks.verifyExtensionJavaScript).not.toHaveBeenCalled();
});
test.each([
[
"extension receipt",
{
verifyExtensionBuildReceipt: () => {
throw new Error("stale extension source");
},
},
],
[
"extension bundles",
{
verifyExtensionJavaScript: () => {
throw new Error("stale extension bundle");
},
},
],
[
"userscript receipt",
{
verifyUserscript: () => {
throw new Error("stale userscript source");
},
},
],
])("propagates a failed %s check before browser attachment", (_label, override) => {
const runtime = _label.startsWith("userscript") ? "userscript" : "extension";
expect(() => assertCurrentLiveArtifact(runtime, BUILD_ID, { ...verifiers(), ...override })).toThrow(/stale/);
});
test("rejects an unsupported live runtime without verifying another artifact", () => {
const checks = verifiers();
expect(() => assertCurrentLiveArtifact("browser", BUILD_ID, checks)).toThrow("Unsupported live artifact runtime");
expect(checks.verifyExtensionBuildReceipt).not.toHaveBeenCalled();
expect(checks.verifyExtensionJavaScript).not.toHaveBeenCalled();
expect(checks.verifyUserscript).not.toHaveBeenCalled();
});
@@ -0,0 +1,390 @@
/** @jest-environment jsdom */
const {
classifyAccountProbeFailure,
inspectVisibleAccountMenu,
isCommittedYoutubePage,
normalizeChannelHandle,
selectAuthenticatedYoutubeContext,
verifyCandidateYoutubePage,
verifyAuthenticatedYoutubePage,
} = require("../e2e/live/live-authenticated-context");
function page(url, { closed = false, context = null } = {}) {
return {
context: () => context,
isClosed: jest.fn(() => closed),
url: jest.fn(() => url),
};
}
function context(pages) {
const value = { pages: jest.fn(() => pages) };
for (const currentPage of pages) currentPage.context = () => value;
return value;
}
function makeHandle(value) {
return {
dispose: jest.fn().mockResolvedValue(undefined),
jsonValue: jest.fn().mockResolvedValue(value),
};
}
describe("authenticated live Chromium context selection", () => {
test.each([
"https://youtube.com/",
"https://www.youtube.com/watch?v=abcdefghijk",
"https://m.youtube.com/shorts/abcdefghijk",
])("accepts a committed ordinary YouTube page at %s", (url) => {
expect(isCommittedYoutubePage(page(url))).toBe(true);
});
test.each([
"about:blank",
"chrome://extensions/",
"chrome-extension://abcdefghijklmnopabcdefghijklmnop/popup.html",
"http://www.youtube.com/",
"https://studio.youtube.com/",
"https://www.youtube.com.example.test/",
"https://user:password@www.youtube.com/",
])("rejects an internal, non-YouTube, or unsafe page at %s", (url) => {
expect(isCommittedYoutubePage(page(url))).toBe(false);
});
test("rejects closed and malformed pages", () => {
expect(isCommittedYoutubePage(page("https://www.youtube.com/", { closed: true }))).toBe(false);
expect(isCommittedYoutubePage(page("not a URL"))).toBe(false);
expect(isCommittedYoutubePage(null)).toBe(false);
});
test("normalizes public handles without accepting an account name or URL", () => {
expect(normalizeChannelHandle(" @Anarios-RYD ")).toBe("@anarios-ryd");
expect(() => normalizeChannelHandle("Anarios RYD")).toThrow(/exact public YouTube @handle/);
expect(() => normalizeChannelHandle("https://youtube.com/@anarios-ryd")).toThrow(/exact public YouTube @handle/);
});
test("scans each context until its profile is resolved and returns the existing matching page", async () => {
const internal = page("chrome://extensions/");
const unrelated = page("https://example.test/");
const wrongYoutube = page("https://www.youtube.com/watch?v=wrongvideo1");
const matchingYoutube = page("https://www.youtube.com/watch?v=rightvideo1");
const laterYoutube = page("https://m.youtube.com/shorts/later-video");
const firstContext = context([internal, wrongYoutube]);
const secondContext = context([unrelated, matchingYoutube, laterYoutube]);
const verifyPage = jest.fn(async (candidate) => candidate === matchingYoutube || candidate === laterYoutube);
const browser = { contexts: jest.fn(() => [firstContext, secondContext]) };
await expect(selectAuthenticatedYoutubeContext(browser, "@Expected", { verifyPage })).resolves.toEqual({
context: secondContext,
contextIndex: 1,
pageIndex: 1,
sessionPage: matchingYoutube,
});
expect(firstContext.pages).toHaveBeenCalledTimes(1);
expect(secondContext.pages).toHaveBeenCalledTimes(2);
expect(verifyPage.mock.calls.map(([candidate]) => candidate)).toEqual([wrongYoutube, matchingYoutube]);
expect(verifyPage).toHaveBeenCalledWith(expect.anything(), "@expected", {
accountProbeTimeoutMilliseconds: 8000,
});
});
test("allows several matching YouTube pages only when they belong to the same context", async () => {
const first = page("https://www.youtube.com/watch?v=abcdefghijk");
const second = page("https://www.youtube.com/shorts/lmnopqrstuv");
const matchingContext = context([first, second]);
await expect(
selectAuthenticatedYoutubeContext({ contexts: () => [matchingContext] }, "@expected", {
verifyPage: async () => true,
}),
).resolves.toMatchObject({ context: matchingContext, pageIndex: 0, sessionPage: first });
});
test("refuses two matching authenticated contexts", async () => {
const first = page("https://www.youtube.com/");
const second = page("https://m.youtube.com/");
await expect(
selectAuthenticatedYoutubeContext({ contexts: () => [context([first]), context([second])] }, "@expected", {
verifyPage: async () => true,
}),
).rejects.toThrow(/More than one attached Chromium context/);
});
test("refuses to select when another candidate context could not be verified", async () => {
const matching = page("https://www.youtube.com/watch?v=abcdefghijk");
const unresolved = page("https://www.youtube.com/watch?v=lmnopqrstuv");
await expect(
selectAuthenticatedYoutubeContext({ contexts: () => [context([matching]), context([unresolved])] }, "@expected", {
verifyPage: async (candidate) => {
if (candidate === unresolved) throw new Error("page disappeared");
return true;
},
}),
).rejects.toThrow(/Could not safely verify every candidate Chromium profile/);
});
test("retries a transient candidate-page failure before selecting its exact authenticated context", async () => {
const matching = page("https://www.youtube.com/watch?v=abcdefghijk");
const matchingContext = context([matching]);
const waitForRetry = jest.fn().mockResolvedValue(undefined);
const timeout = Object.assign(new Error("account menu did not hydrate"), { name: "TimeoutError" });
const verifyPage = jest.fn().mockRejectedValueOnce(timeout).mockResolvedValueOnce(true);
const contexts = jest.fn(() => [matchingContext]);
await expect(
selectAuthenticatedYoutubeContext({ contexts }, "@expected", {
accountSelectionRetryDelayMilliseconds: 10,
verifyPage,
waitForRetry,
}),
).resolves.toMatchObject({ context: matchingContext, pageIndex: 0, sessionPage: matching });
expect(verifyPage).toHaveBeenCalledTimes(2);
expect(waitForRetry).toHaveBeenCalledWith(10);
expect(contexts).toHaveBeenCalledTimes(2);
});
test("re-enumerates pages and can select a matching YouTube page which commits before the second pass", async () => {
const loading = page("about:blank");
const matching = page("https://www.youtube.com/watch?v=abcdefghijk");
const candidateContext = {
pages: jest.fn().mockReturnValueOnce([loading]).mockReturnValue([matching]),
};
loading.context = matching.context = () => candidateContext;
const contexts = jest.fn(() => [candidateContext]);
const verifyPage = jest.fn().mockResolvedValue(true);
await expect(
selectAuthenticatedYoutubeContext({ contexts }, "@expected", {
verifyPage,
waitForRetry: jest.fn().mockResolvedValue(undefined),
}),
).resolves.toMatchObject({ context: candidateContext, sessionPage: matching });
expect(contexts).toHaveBeenCalledTimes(2);
expect(verifyPage).toHaveBeenCalledTimes(1);
});
test("rescans instead of returning a selected page which closed after its account proof", async () => {
const closedAfterProof = page("https://www.youtube.com/watch?v=abcdefghijk");
const replacement = page("https://www.youtube.com/watch?v=lmnopqrstuv");
const candidateContext = {
pages: jest.fn().mockReturnValueOnce([closedAfterProof]).mockReturnValue([replacement]),
};
closedAfterProof.context = replacement.context = () => candidateContext;
const verifyPage = jest.fn(async (candidate) => {
if (candidate === closedAfterProof) closedAfterProof.isClosed.mockReturnValue(true);
return true;
});
await expect(
selectAuthenticatedYoutubeContext({ contexts: () => [candidateContext] }, "@expected", {
verifyPage,
waitForRetry: jest.fn().mockResolvedValue(undefined),
}),
).resolves.toMatchObject({ context: candidateContext, sessionPage: replacement });
expect(verifyPage.mock.calls.map(([candidate]) => candidate)).toEqual([closedAfterProof, replacement]);
});
test("keeps a repeatedly failing committed candidate fail-closed and reports only a sanitized reason", async () => {
const unresolved = page("https://www.youtube.com/watch?v=abcdefghijk");
const privateFailure = Object.assign(new Error("private-page-detail@example.test"), { name: "TimeoutError" });
const selection = selectAuthenticatedYoutubeContext({ contexts: () => [context([unresolved])] }, "@expected", {
verifyPage: jest.fn().mockRejectedValue(privateFailure),
waitForRetry: jest.fn().mockResolvedValue(undefined),
});
await expect(selection).rejects.toThrow(
"Could not safely verify every candidate Chromium profile for @expected; no extension was changed. Unresolved probes: context 1 page 1 (timeout).",
);
await expect(selection).rejects.not.toThrow(/private-page-detail/);
});
test("ignores a candidate which leaves YouTube after a transient probe failure", async () => {
const matching = page("https://www.youtube.com/watch?v=abcdefghijk");
const stale = page("https://www.youtube.com/watch?v=lmnopqrstuv");
const matchingContext = context([matching]);
context([stale]);
const verifyPage = jest.fn(async (candidate) => {
if (candidate === matching) return true;
stale.url.mockReturnValue("about:blank");
throw new Error("Execution context was destroyed");
});
await expect(
selectAuthenticatedYoutubeContext({ contexts: () => [matchingContext, stale.context()] }, "@expected", {
verifyPage,
waitForRetry: jest.fn(),
}),
).resolves.toMatchObject({ context: matchingContext, sessionPage: matching });
expect(verifyPage).toHaveBeenCalledTimes(2);
});
test.each([
[Object.assign(new Error("menu timeout"), { name: "TimeoutError" }), "timeout"],
[new Error("Target page has been closed"), "page-closed"],
[new Error("Execution context was destroyed, most likely because of a navigation"), "navigation-race"],
[new Error("unexpected failure"), "probe-error"],
])("classifies account-probe failures without returning their message", (error, expectedKind) => {
expect(classifyAccountProbeFailure(error)).toBe(expectedKind);
});
test("resolves a candidate that closes during a failed probe as a non-match", async () => {
const candidate = page("https://www.youtube.com/");
const result = await verifyCandidateYoutubePage(
candidate,
"@expected",
async () => {
candidate.isClosed.mockReturnValue(true);
throw new Error("Target page has been closed");
},
{ waitForRetry: jest.fn() },
);
expect(result).toEqual({ matched: false, resolved: true });
});
test("reports separately when there are no YouTube pages and when no account matches", async () => {
await expect(
selectAuthenticatedYoutubeContext(
{ contexts: () => [context([page("chrome://extensions/"), page("https://example.test/")])] },
"@expected",
{ verifyPage: jest.fn() },
),
).rejects.toThrow(/No pre-existing committed HTTPS YouTube page/);
await expect(
selectAuthenticatedYoutubeContext(
{ contexts: () => [context([page("https://www.youtube.com/")])] },
"@expected",
{ verifyPage: async () => false },
),
).rejects.toThrow(/No attached Chromium context has a pre-existing signed-in YouTube page/);
});
test("uses only scoped visible account-menu links and compares the exact handle", () => {
document.body.innerHTML = `
<a id="content-link" href="https://www.youtube.com/@expected">Unrelated video description link</a>
<ytd-multi-page-menu-renderer>
<a id="account-link" href="https://www.youtube.com/@expected/">View your channel</a>
</ytd-multi-page-menu-renderer>
`;
for (const element of document.querySelectorAll("ytd-multi-page-menu-renderer, a")) {
element.getBoundingClientRect = () => ({ height: 20, width: 100 });
}
expect(inspectVisibleAccountMenu("@expected")).toEqual({ matchedExpectedHandle: true, ready: true });
expect(inspectVisibleAccountMenu("@different")).toEqual({ matchedExpectedHandle: false, ready: true });
document.querySelector("ytd-multi-page-menu-renderer").remove();
expect(inspectVisibleAccountMenu("@expected")).toBeNull();
});
test("can verify the active handle from the account header without returning account text", () => {
document.body.innerHTML = `
<ytd-multi-page-menu-renderer>
<ytd-active-account-header-renderer><span>Private Name private@example.test</span><span id="channel-handle">@Expected</span></ytd-active-account-header-renderer>
</ytd-multi-page-menu-renderer>
`;
for (const element of document.querySelectorAll(
"ytd-multi-page-menu-renderer, ytd-active-account-header-renderer, span",
)) {
element.getBoundingClientRect = () => ({ height: 20, width: 100 });
}
const result = inspectVisibleAccountMenu("@expected");
expect(result).toEqual({ matchedExpectedHandle: true, ready: true });
expect(JSON.stringify(result)).not.toContain("private@example.test");
expect(JSON.stringify(result)).not.toContain("Private Name");
});
test("verifies a signed-in page by opening and restoring the account menu", async () => {
const menuStates = [null, { matchedExpectedHandle: true, ready: true }];
const avatar = { click: jest.fn().mockResolvedValue(undefined), isVisible: jest.fn().mockResolvedValue(true) };
const avatars = { count: jest.fn().mockResolvedValue(1), nth: jest.fn(() => avatar) };
const browserPage = {
bringToFront: jest.fn().mockResolvedValue(undefined),
evaluate: jest
.fn()
.mockResolvedValueOnce({ committed: true, configuredLoggedIn: true })
.mockResolvedValueOnce(menuStates.shift()),
isClosed: () => false,
keyboard: { press: jest.fn().mockResolvedValue(undefined) },
locator: jest.fn(() => avatars),
url: () => "https://www.youtube.com/watch?v=abcdefghijk",
waitForFunction: jest.fn().mockResolvedValue(makeHandle(menuStates.shift())),
};
await expect(verifyAuthenticatedYoutubePage(browserPage, "@Expected")).resolves.toBe(true);
expect(browserPage.bringToFront).toHaveBeenCalledTimes(1);
expect(avatar.click).toHaveBeenCalledWith({ timeout: 8000 });
expect(browserPage.keyboard.press).toHaveBeenCalledWith("Escape");
expect(browserPage.waitForFunction).toHaveBeenCalledWith(inspectVisibleAccountMenu, "@expected", {
polling: 100,
timeout: 8000,
});
});
test("rejects an explicitly signed-out page without opening the account menu", async () => {
const browserPage = {
evaluate: jest.fn().mockResolvedValue({ committed: true, configuredLoggedIn: false }),
isClosed: () => false,
locator: jest.fn(),
url: () => "https://www.youtube.com/",
};
await expect(verifyAuthenticatedYoutubePage(browserPage, "@expected")).resolves.toBe(false);
expect(browserPage.locator).not.toHaveBeenCalled();
});
test("surfaces an account-menu timeout or broken page as an unresolved profile", async () => {
const timeout = Object.assign(new Error("menu did not appear"), { name: "TimeoutError" });
const createBrowserPage = (waitError) => {
const avatar = { click: jest.fn().mockResolvedValue(undefined), isVisible: jest.fn().mockResolvedValue(true) };
return {
evaluate: jest
.fn()
.mockResolvedValueOnce({ committed: true, configuredLoggedIn: true })
.mockResolvedValueOnce(null),
isClosed: () => false,
keyboard: { press: jest.fn().mockResolvedValue(undefined) },
locator: jest.fn(() => ({ count: async () => 1, nth: () => avatar })),
url: () => "https://www.youtube.com/",
waitForFunction: jest.fn().mockRejectedValue(waitError),
};
};
await expect(verifyAuthenticatedYoutubePage(createBrowserPage(timeout), "@expected")).rejects.toThrow(
/menu did not appear/,
);
await expect(
verifyAuthenticatedYoutubePage(createBrowserPage(new Error("Target page has been closed")), "@expected"),
).rejects.toThrow(/Target page has been closed/);
});
test("keeps a serialized account-menu match when its remote handle cannot be disposed during navigation", async () => {
const menuHandle = makeHandle({ matchedExpectedHandle: true, ready: true });
menuHandle.dispose.mockRejectedValue(new Error("Execution context was destroyed"));
const avatar = { click: jest.fn().mockResolvedValue(undefined), isVisible: jest.fn().mockResolvedValue(true) };
const browserPage = {
evaluate: jest
.fn()
.mockResolvedValueOnce({ committed: true, configuredLoggedIn: true })
.mockResolvedValueOnce(null),
isClosed: () => false,
keyboard: { press: jest.fn().mockResolvedValue(undefined) },
locator: jest.fn(() => ({ count: async () => 1, nth: () => avatar })),
url: () => "https://www.youtube.com/",
waitForFunction: jest.fn().mockResolvedValue(menuHandle),
};
await expect(verifyAuthenticatedYoutubePage(browserPage, "@expected")).resolves.toBe(true);
expect(menuHandle.dispose).toHaveBeenCalledTimes(1);
});
test("does not read or return cookies, storage, account names, or email addresses", () => {
const probeSource = `${inspectVisibleAccountMenu}\n${verifyAuthenticatedYoutubePage}`;
expect(probeSource).not.toMatch(/\.cookie|localStorage|sessionStorage|\.email|accountName/);
});
});
@@ -0,0 +1,233 @@
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const ACTIVE_PORT_FILE_NAME = "DevToolsActivePort";
const BROWSER_ALIASES = new Set(["brave", "chrome", "chromium", "edge"]);
function nonEmptyEnvironmentValue(environment, name) {
const value = environment[name]?.trim();
return value || null;
}
function platformPath(platform, pathImpl) {
if (pathImpl) return pathImpl;
return platform === "win32" ? path.win32 : path.posix;
}
function browserUserDataDirectories(browser, { environment, homedir, pathImpl, platform }) {
const join = pathImpl.join.bind(pathImpl);
const home = nonEmptyEnvironmentValue(environment, "HOME") || homedir();
if (platform === "win32") {
const localAppData = nonEmptyEnvironmentValue(environment, "LOCALAPPDATA");
if (!localAppData) return [];
const directories = {
brave: [join(localAppData, "BraveSoftware", "Brave-Browser", "User Data")],
chrome: [
join(localAppData, "Google", "Chrome", "User Data"),
join(localAppData, "Google", "Chrome Beta", "User Data"),
join(localAppData, "Google", "Chrome Dev", "User Data"),
join(localAppData, "Google", "Chrome SxS", "User Data"),
],
chromium: [join(localAppData, "Chromium", "User Data")],
edge: [join(localAppData, "Microsoft", "Edge", "User Data")],
};
return directories[browser] || [];
}
if (platform === "darwin") {
const applicationSupport = join(home, "Library", "Application Support");
const directories = {
brave: [join(applicationSupport, "BraveSoftware", "Brave-Browser")],
chrome: [
join(applicationSupport, "Google", "Chrome"),
join(applicationSupport, "Google", "Chrome Beta"),
join(applicationSupport, "Google", "Chrome Dev"),
join(applicationSupport, "Google", "Chrome Canary"),
],
chromium: [join(applicationSupport, "Chromium")],
edge: [join(applicationSupport, "Microsoft Edge")],
};
return directories[browser] || [];
}
const configRoot = nonEmptyEnvironmentValue(environment, "XDG_CONFIG_HOME") || join(home, ".config");
const directories = {
brave: [join(configRoot, "BraveSoftware", "Brave-Browser")],
chrome: [
join(configRoot, "google-chrome"),
join(configRoot, "google-chrome-beta"),
join(configRoot, "google-chrome-unstable"),
],
chromium: [join(configRoot, "chromium")],
edge: [join(configRoot, "microsoft-edge")],
};
return directories[browser] || [];
}
function knownActivePortFiles(browser, settings) {
const browsers = browser ? [browser] : ["chrome", "brave", "edge", "chromium"];
return browsers.flatMap((name) =>
browserUserDataDirectories(name, settings).map((directory) =>
settings.pathImpl.join(directory, ACTIVE_PORT_FILE_NAME),
),
);
}
function parseBareHostAndPort(value) {
let url;
try {
url = new URL(`http://${value}`);
} catch {
return null;
}
if (!url.hostname || !url.port || url.username || url.password || url.pathname !== "/" || url.search || url.hash) {
return null;
}
const port = Number(url.port);
if (!Number.isSafeInteger(port) || port < 1 || port > 65535) return null;
return { authority: url.host, port };
}
function parseActivePortFile(contents, filePath) {
const [rawPort, rawBrowserPath] = String(contents).split(/\r?\n/);
const port = Number(rawPort?.trim());
if (!/^\d+$/.test(rawPort?.trim() || "") || !Number.isSafeInteger(port) || port < 1 || port > 65535) {
throw new Error(`The CDP active-port file at ${filePath} has an invalid port.`);
}
let browserPath = rawBrowserPath?.trim();
if (/^wss?:\/\//i.test(browserPath || "")) {
let url;
try {
url = new URL(browserPath);
} catch {
throw new Error(`The CDP active-port file at ${filePath} has an invalid browser WebSocket URL.`);
}
browserPath = `${url.pathname}${url.search}`;
}
if (!/^\/devtools\/browser\/[A-Za-z0-9._-]+$/.test(browserPath || "")) {
throw new Error(`The CDP active-port file at ${filePath} has an invalid browser WebSocket path.`);
}
return { browserPath, port };
}
function unique(values, platform) {
const keys = new Set();
return values.filter((value) => {
const key = platform === "win32" ? value.toLowerCase() : value;
if (keys.has(key)) return false;
keys.add(key);
return true;
});
}
function activePortFileCandidates(browser, settings) {
const activePortFile = nonEmptyEnvironmentValue(settings.environment, "RYD_CDP_ACTIVE_PORT_FILE");
const userDataDirectory = nonEmptyEnvironmentValue(settings.environment, "RYD_CDP_USER_DATA_DIR");
if (activePortFile && userDataDirectory) {
throw new Error("Set only one of RYD_CDP_ACTIVE_PORT_FILE and RYD_CDP_USER_DATA_DIR.");
}
if (activePortFile) return { explicit: true, paths: [settings.pathImpl.resolve(activePortFile)] };
if (userDataDirectory) {
return {
explicit: true,
paths: [settings.pathImpl.join(settings.pathImpl.resolve(userDataDirectory), ACTIVE_PORT_FILE_NAME)],
};
}
return { explicit: false, paths: unique(knownActivePortFiles(browser, settings), settings.platform) };
}
function readCandidate(filePath, settings, explicit) {
try {
return parseActivePortFile(settings.readFileSync(filePath, "utf8"), filePath);
} catch (error) {
if (!explicit && ["ENOENT", "ENOTDIR"].includes(error.code)) return null;
throw error;
}
}
function protocolEndpoint(value) {
if (!/^(?:https?|wss?):\/\//i.test(value)) return null;
let url;
try {
url = new URL(value);
} catch {
throw new Error("RYD_CDP_ENDPOINT must be a valid HTTP(S), WebSocket, host:port, or browser-alias endpoint.");
}
if (!["http:", "https:", "ws:", "wss:"].includes(url.protocol) || !url.hostname) {
throw new Error("RYD_CDP_ENDPOINT must be a valid HTTP(S), WebSocket, host:port, or browser-alias endpoint.");
}
return value;
}
function resolveCdpEndpoint(
configuredEndpoint,
{
environment = process.env,
homedir = os.homedir,
pathImpl,
platform = process.platform,
readFileSync = fs.readFileSync,
} = {},
) {
const value = configuredEndpoint?.trim() || "chrome";
const explicitProtocolEndpoint = protocolEndpoint(value);
if (explicitProtocolEndpoint) return explicitProtocolEndpoint;
const browser = BROWSER_ALIASES.has(value.toLowerCase()) ? value.toLowerCase() : null;
const bareEndpoint = browser ? null : parseBareHostAndPort(value);
if (!browser && !bareEndpoint) {
throw new Error(
"RYD_CDP_ENDPOINT must be an HTTP(S) URL, WebSocket URL, host:port shown by Chromium, or one of chrome, brave, edge, and chromium.",
);
}
const settings = {
environment,
homedir,
pathImpl: platformPath(platform, pathImpl),
platform,
readFileSync,
};
const candidates = activePortFileCandidates(browser, settings);
const matches = [];
for (const filePath of candidates.paths) {
const activePort = readCandidate(filePath, settings, candidates.explicit);
if (!activePort || (bareEndpoint && activePort.port !== bareEndpoint.port)) continue;
const authority = bareEndpoint?.authority || `127.0.0.1:${activePort.port}`;
matches.push({ endpoint: `ws://${authority}${activePort.browserPath}`, filePath });
}
const endpoints = [...new Set(matches.map(({ endpoint }) => endpoint))];
if (endpoints.length === 1) return endpoints[0];
if (endpoints.length > 1) {
throw new Error(
`More than one DevToolsActivePort file matches RYD_CDP_ENDPOINT=${value}. Set RYD_CDP_ACTIVE_PORT_FILE or RYD_CDP_USER_DATA_DIR to select the intended browser profile.`,
);
}
const fileDescription = candidates.explicit
? candidates.paths[0]
: candidates.paths.length
? candidates.paths.join(", ")
: "the platform's standard browser user-data directories";
const portDescription = bareEndpoint ? ` matching port ${bareEndpoint.port}` : "";
throw new Error(
`Could not find a valid DevToolsActivePort file${portDescription} in ${fileDescription}. ` +
"Set RYD_CDP_ACTIVE_PORT_FILE to that file, RYD_CDP_USER_DATA_DIR to its directory, or use an explicit ws:// or legacy http:// endpoint.",
);
}
module.exports = {
ACTIVE_PORT_FILE_NAME,
browserUserDataDirectories,
parseActivePortFile,
parseBareHostAndPort,
resolveCdpEndpoint,
};
@@ -0,0 +1,226 @@
const path = require("node:path");
const {
browserUserDataDirectories,
parseActivePortFile,
parseBareHostAndPort,
resolveCdpEndpoint,
} = require("./live-cdp-endpoint");
function missingFile(filePath) {
throw Object.assign(new Error(`missing ${filePath}`), { code: "ENOENT" });
}
function mappedReader(files) {
return jest.fn((filePath) => {
if (Object.prototype.hasOwnProperty.call(files, filePath)) return files[filePath];
return missingFile(filePath);
});
}
describe("live CDP endpoint resolver", () => {
test.each([
"ws://127.0.0.1:60011/devtools/browser/session-id",
"wss://debug.example.test/devtools/browser/session-id",
"http://127.0.0.1:9222",
"https://debug.example.test:9222",
])("preserves an explicit protocol endpoint without reading the filesystem: %s", (endpoint) => {
const readFileSync = jest.fn();
expect(resolveCdpEndpoint(endpoint, { readFileSync })).toBe(endpoint);
expect(readFileSync).not.toHaveBeenCalled();
});
test("resolves Chrome's host:port-only UI value through the matching Windows active-port file", () => {
const chromeFile = "C:\\Users\\tester\\AppData\\Local\\Google\\Chrome\\User Data\\DevToolsActivePort";
const braveFile = "C:\\Users\\tester\\AppData\\Local\\BraveSoftware\\Brave-Browser\\User Data\\DevToolsActivePort";
const readFileSync = mappedReader({
[braveFile]: "50000\n/devtools/browser/brave-session\n",
[chromeFile]: "60011\r\n/devtools/browser/chrome-session\r\n",
});
expect(
resolveCdpEndpoint("127.0.0.1:60011", {
environment: { LOCALAPPDATA: "C:\\Users\\tester\\AppData\\Local" },
homedir: () => "C:\\Users\\tester",
pathImpl: path.win32,
platform: "win32",
readFileSync,
}),
).toBe("ws://127.0.0.1:60011/devtools/browser/chrome-session");
expect(readFileSync).toHaveBeenCalledWith(chromeFile, "utf8");
});
test("uses the configured bare hostname while taking the browser path from the file", () => {
const activePortFile = "C:\\portable\\profile\\DevToolsActivePort";
expect(
resolveCdpEndpoint("localhost:60011", {
environment: { RYD_CDP_USER_DATA_DIR: "C:\\portable\\profile" },
pathImpl: path.win32,
platform: "win32",
readFileSync: mappedReader({ [activePortFile]: "60011\n/devtools/browser/portable-session\n" }),
}),
).toBe("ws://localhost:60011/devtools/browser/portable-session");
});
test("supports a bracketed IPv6 loopback host", () => {
expect(
resolveCdpEndpoint("[::1]:60011", {
environment: { RYD_CDP_ACTIVE_PORT_FILE: "/profile/DevToolsActivePort" },
pathImpl: path.posix,
platform: "linux",
readFileSync: mappedReader({
"/profile/DevToolsActivePort": "60011\n/devtools/browser/ipv6-session\n",
}),
}),
).toBe("ws://[::1]:60011/devtools/browser/ipv6-session");
});
test.each([
[
"win32",
path.win32,
{ LOCALAPPDATA: "C:\\Users\\tester\\AppData\\Local" },
() => "C:\\Users\\tester",
"chrome",
"C:\\Users\\tester\\AppData\\Local\\Google\\Chrome\\User Data",
],
[
"darwin",
path.posix,
{ HOME: "/Users/tester" },
() => "/fallback",
"brave",
"/Users/tester/Library/Application Support/BraveSoftware/Brave-Browser",
],
[
"linux",
path.posix,
{ HOME: "/home/tester", XDG_CONFIG_HOME: "/custom/config" },
() => "/fallback",
"edge",
"/custom/config/microsoft-edge",
],
])("discovers standard %s browser user-data roots", (platform, pathImpl, environment, homedir, browser, expected) => {
expect(browserUserDataDirectories(browser, { environment, homedir, pathImpl, platform })).toContain(expected);
});
test.each(["chrome", "CHROME", "brave", "edge", "chromium", undefined])(
"resolves browser alias %p from its active-port file",
(alias) => {
const activePortFile = "/custom/profile/DevToolsActivePort";
expect(
resolveCdpEndpoint(alias, {
environment: { RYD_CDP_ACTIVE_PORT_FILE: activePortFile },
pathImpl: path.posix,
platform: "linux",
readFileSync: mappedReader({
[activePortFile]: "60123\n/devtools/browser/alias-session\n",
}),
}),
).toBe("ws://127.0.0.1:60123/devtools/browser/alias-session");
},
);
test("accepts an absolute WebSocket URL in an active-port file but uses the configured authority", () => {
expect(
resolveCdpEndpoint("localhost:60011", {
environment: { RYD_CDP_ACTIVE_PORT_FILE: "/profile/DevToolsActivePort" },
pathImpl: path.posix,
platform: "linux",
readFileSync: mappedReader({
"/profile/DevToolsActivePort": "60011\nws://127.0.0.1:60011/devtools/browser/full-url-session\n",
}),
}),
).toBe("ws://localhost:60011/devtools/browser/full-url-session");
});
test("uses an exact active-port-file override", () => {
const readFileSync = mappedReader({
"/custom/ChromeActivePort": "61234\n/devtools/browser/custom-session\n",
});
expect(
resolveCdpEndpoint("chrome", {
environment: { RYD_CDP_ACTIVE_PORT_FILE: "/custom/ChromeActivePort" },
pathImpl: path.posix,
platform: "linux",
readFileSync,
}),
).toBe("ws://127.0.0.1:61234/devtools/browser/custom-session");
expect(readFileSync).toHaveBeenCalledTimes(1);
});
test("rejects a port mismatch instead of attaching to a different browser", () => {
expect(() =>
resolveCdpEndpoint("127.0.0.1:60011", {
environment: { RYD_CDP_ACTIVE_PORT_FILE: "/profile/DevToolsActivePort" },
pathImpl: path.posix,
platform: "linux",
readFileSync: mappedReader({
"/profile/DevToolsActivePort": "60012\n/devtools/browser/wrong-browser\n",
}),
}),
).toThrow(/matching port 60011/);
});
test("rejects ambiguous standard profiles with the same port and different browser paths", () => {
const localAppData = "C:\\Users\\tester\\AppData\\Local";
const chromeFile = `${localAppData}\\Google\\Chrome\\User Data\\DevToolsActivePort`;
const braveFile = `${localAppData}\\BraveSoftware\\Brave-Browser\\User Data\\DevToolsActivePort`;
expect(() =>
resolveCdpEndpoint("127.0.0.1:60011", {
environment: { LOCALAPPDATA: localAppData },
pathImpl: path.win32,
platform: "win32",
readFileSync: mappedReader({
[chromeFile]: "60011\n/devtools/browser/chrome-session\n",
[braveFile]: "60011\n/devtools/browser/brave-session\n",
}),
}),
).toThrow(/More than one DevToolsActivePort file matches/);
});
test("rejects conflicting file and user-data-directory overrides", () => {
expect(() =>
resolveCdpEndpoint("chrome", {
environment: {
RYD_CDP_ACTIVE_PORT_FILE: "/profile/DevToolsActivePort",
RYD_CDP_USER_DATA_DIR: "/profile",
},
pathImpl: path.posix,
platform: "linux",
}),
).toThrow(/Set only one/);
});
test.each([
["not-a-port\n/devtools/browser/session\n", /invalid port/],
["70000\n/devtools/browser/session\n", /invalid port/],
["60011\n/devtools/page/not-a-browser\n", /invalid browser WebSocket path/],
["60011\n", /invalid browser WebSocket path/],
["60011\nws://bad host/devtools/browser/session\n", /invalid browser WebSocket URL/],
])("rejects a malformed active-port file", (contents, expectedError) => {
expect(() => parseActivePortFile(contents, "DevToolsActivePort")).toThrow(expectedError);
});
test.each([
["127.0.0.1:60011", { authority: "127.0.0.1:60011", port: 60011 }],
["localhost:9222", { authority: "localhost:9222", port: 9222 }],
["[::1]:1234", { authority: "[::1]:1234", port: 1234 }],
["localhost", null],
["localhost:0", null],
["localhost:70000", null],
["localhost:9222/json/version", null],
])("parses bare host/port endpoint %p", (endpoint, expected) => {
expect(parseBareHostAndPort(endpoint)).toEqual(expected);
});
test.each(["not-an-endpoint", "ftp://127.0.0.1:9222", "127.0.0.1", "127.0.0.1:70000/path"])(
"rejects unsupported endpoint %p",
(endpoint) => {
expect(() => resolveCdpEndpoint(endpoint)).toThrow(/RYD_CDP_ENDPOINT/);
},
);
});
@@ -4,10 +4,14 @@
const path = require("node:path");
const {
LiveFatalSignalGuard,
LiveReadOnlyGate,
LiveRunDiagnostics,
UNHANDLED_REJECTION_PREFIX,
diagnosticApiUrl,
environmentalConsoleWarningId,
readLivePageState,
runIndependentLoggedStages,
runLoggedStage,
} = require("../e2e/live/live-diagnostics");
@@ -34,6 +38,29 @@ function visibleRect() {
return { bottom: 100, height: 90, left: 10, right: 60, top: 10, width: 50 };
}
function consoleError(text, url = "https://www.youtube.com/watch?v=abcdefghijk") {
return {
location: () => ({ lineNumber: 7, url }),
text: () => text,
type: () => "error",
};
}
function createLiveHarness(diagnosticsOptions = {}) {
const context = new FakeEmitter();
context.serviceWorkers = jest.fn(() => []);
const page = new FakeEmitter();
page.addInitScript = jest.fn().mockResolvedValue(undefined);
page.evaluate = jest.fn().mockResolvedValue(undefined);
page.isClosed = jest.fn(() => false);
page.url = jest.fn(() => "https://www.youtube.com/watch?v=abcdefghijk");
return {
context,
diagnostics: new LiveRunDiagnostics(page, context, { log: jest.fn(), ...diagnosticsOptions }),
page,
};
}
describe("live interactive diagnostics", () => {
test("captures runtime and Shorts ownership state without dumping page HTML", () => {
document.documentElement.setAttribute("data-ryd-userscript-version", "3.2.0");
@@ -159,6 +186,417 @@ describe("live interactive diagnostics", () => {
expect(diagnostics.stageFailed).toHaveBeenCalledWith("failure", expect.any(Number), failure);
});
test.each([
["pageerror", (page) => page.emit("pageerror", new Error("page exploded"))],
[
"unhandledrejection",
(page) => page.emit("console", consoleError(`${UNHANDLED_REJECTION_PREFIX}promise exploded`)),
],
["console.error", (page) => page.emit("console", consoleError("console exploded"))],
])("turns an otherwise-passing stage red for a %s", async (type, emitSignal) => {
const { diagnostics, page } = createLiveHarness();
await diagnostics.start();
await expect(
runLoggedStage(diagnostics, "read-only.watch", async () => {
emitSignal(page);
return "rendered";
}),
).rejects.toMatchObject({ name: "LiveFatalSignalError" });
expect(diagnostics.browserSignals).toEqual([
expect.objectContaining({ ignoredBy: null, stage: "read-only.watch", type }),
]);
diagnostics.stop();
});
test.each([
[
"googlevideo HTTP 403",
"Failed to load resource: the server responded with a status of 403 ()",
"https://rr2---sn-q4flrnel.googlevideo.com/videoplayback?opaque=1",
"googlevideo-resource-failure",
],
[
"googlevideo DNS failure",
"Failed to load resource: net::ERR_NAME_NOT_RESOLVED",
"https://rr3---sn-oxup5-f5fk.googlevideo.com/generate_204",
"googlevideo-resource-failure",
],
[
"doubleclick follow-on network failure",
"Failed to load resource: net::ERR_FAILED",
"https://googleads.g.doubleclick.net/pagead/viewthroughconversion/123/",
"doubleclick-resource-failure",
],
[
"doubleclick CORS failure",
"Access to XMLHttpRequest at 'https://googleads.g.doubleclick.net/pagead/id' from origin 'https://www.youtube.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.",
"https://www.youtube.com/watch?v=abcdefghijk",
"doubleclick-cors",
],
[
"doubleclick CORS failure after a YouTube ad redirect",
"Access to fetch at 'https://googleads.g.doubleclick.net/pagead/viewthroughconversion/962985656/?backend=innertube&cname=1' (redirected from 'https://www.youtube.com/pagead/viewthroughconversion/962985656/?backend=innertube&cname=1') from origin 'https://www.youtube.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.",
"https://www.youtube.com/watch?v=abcdefghijk&list=RDabcdefghijk",
"doubleclick-cors",
],
[
"sandboxed about:blank frame script block",
"Blocked script execution in 'about:blank' because the document's frame is sandboxed and the 'allow-scripts' permission is not set.",
"about:blank",
"sandboxed-about-blank-script",
],
[
"YouTube account-cookie rotation rate limit",
"Failed to load resource: the server responded with a status of 429 ()",
"https://accounts.youtube.com/RotateCookies",
"youtube-rotate-cookies-rate-limit",
],
[
"YouTube accounts report-only frame-ancestors violation",
"Framing 'https://accounts.youtube.com/' violates the following report-only Content Security Policy directive: \"frame-ancestors 'self'\". The violation has been logged, but no further action has been taken.\n",
"",
"youtube-accounts-report-only-frame-ancestors",
],
])("records a narrowly recognized external %s as a non-fatal warning", async (_name, message, url, warningId) => {
const { diagnostics, page } = createLiveHarness();
await diagnostics.start();
await expect(
runLoggedStage(diagnostics, "read-only.watch", async () => {
page.emit("console", consoleError(message, url));
return "rendered";
}),
).resolves.toBe("rendered");
expect(diagnostics.browserSignals).toEqual([
expect.objectContaining({
environmentalWarning: warningId,
ignoredBy: null,
severity: "warning",
stage: "read-only.watch",
type: "console.error",
}),
]);
diagnostics.stop();
});
test.each([
[
"unknown status on a media CDN",
"Failed to load resource: the server responded with a status of 404 ()",
"https://rr2---sn-q4flrnel.googlevideo.com/videoplayback",
],
[
"page JavaScript failure reported at a media URL",
"TypeError: Cannot read properties of undefined",
"https://rr2---sn-q4flrnel.googlevideo.com/videoplayback",
],
[
"known network wording on YouTube itself",
"Failed to load resource: net::ERR_NAME_NOT_RESOLVED",
"https://www.youtube.com/youtubei/v1/player",
],
[
"production RYD API failure",
"Failed to load resource: net::ERR_FAILED",
"https://returnyoutubedislikeapi.com/votes?videoId=abcdefghijk",
],
[
"lookalike media hostname",
"Failed to load resource: the server responded with a status of 403 ()",
"https://googlevideo.com.attacker.test/videoplayback",
],
[
"insecure media URL",
"Failed to load resource: net::ERR_NAME_NOT_RESOLVED",
"http://rr2---sn-q4flrnel.googlevideo.com/videoplayback",
],
[
"nonstandard media port",
"Failed to load resource: net::ERR_NAME_NOT_RESOLVED",
"https://rr2---sn-q4flrnel.googlevideo.com:444/videoplayback",
],
[
"different YouTube account endpoint",
"Failed to load resource: the server responded with a status of 429 ()",
"https://accounts.youtube.com/RotateCookiesAgain",
],
[
"different status from YouTube cookie rotation",
"Failed to load resource: the server responded with a status of 500 ()",
"https://accounts.youtube.com/RotateCookies",
],
[
"lookalike YouTube accounts hostname",
"Failed to load resource: the server responded with a status of 429 ()",
"https://accounts.youtube.com.attacker.test/RotateCookies",
],
[
"enforced YouTube accounts frame-ancestors violation",
"Framing 'https://accounts.youtube.com/' violates the following Content Security Policy directive: \"frame-ancestors 'self'\".",
"",
],
[
"report-only YouTube accounts frame-ancestors near miss",
"Framing 'https://accounts.youtube.com/' violates the following report-only Content Security Policy directive: \"frame-ancestors 'self'\". The violation has been logged, but no further action has been taken.",
"",
],
[
"unrelated report-only accounts frame-ancestors violation",
"Framing 'https://accounts.google.com/' violates the following report-only Content Security Policy directive: \"frame-ancestors 'self'\". The violation has been logged, but no further action has been taken.\n",
"",
],
[
"YouTube accounts report-only frame-ancestors violation with a page location",
"Framing 'https://accounts.youtube.com/' violates the following report-only Content Security Policy directive: \"frame-ancestors 'self'\". The violation has been logged, but no further action has been taken.\n",
"https://www.youtube.com/watch?v=abcdefghijk",
],
[
"lookalike CORS target",
"Access to fetch at 'https://doubleclick.net.attacker.test/pixel' from origin 'https://www.youtube.com' has been blocked by CORS policy: blocked",
"https://www.youtube.com/watch?v=abcdefghijk",
],
[
"non-YouTube CORS origin",
"Access to fetch at 'https://googleads.g.doubleclick.net/pixel' from origin 'https://attacker.test' has been blocked by CORS policy: blocked",
"https://www.youtube.com/watch?v=abcdefghijk",
],
[
"doubleclick CORS redirect from a foreign origin",
"Access to fetch at 'https://googleads.g.doubleclick.net/pagead/viewthroughconversion/123/' (redirected from 'https://attacker.test/pagead/viewthroughconversion/123/') from origin 'https://www.youtube.com' has been blocked by CORS policy: blocked",
"https://www.youtube.com/watch?v=abcdefghijk",
],
[
"doubleclick CORS redirect from a different YouTube endpoint",
"Access to fetch at 'https://googleads.g.doubleclick.net/pagead/viewthroughconversion/123/' (redirected from 'https://www.youtube.com/youtubei/v1/player') from origin 'https://www.youtube.com' has been blocked by CORS policy: blocked",
"https://www.youtube.com/watch?v=abcdefghijk",
],
[
"sandboxed-frame message at a page URL",
"Blocked script execution in 'about:blank' because the document's frame is sandboxed and the 'allow-scripts' permission is not set.",
"https://www.youtube.com/watch?v=abcdefghijk",
],
[
"near-miss sandboxed-frame message",
"Blocked script execution in 'about:blank' because the document's frame is sandboxed and the 'allow-scripts' permission is not set. Unexpected",
"about:blank",
],
["unrelated about:blank JavaScript error", "TypeError: Cannot read properties of undefined", "about:blank"],
])("keeps %s fatal", async (_name, message, url) => {
const { diagnostics, page } = createLiveHarness();
await diagnostics.start();
page.emit("console", consoleError(message, url));
expect(() => diagnostics.consumeFatalSignals("unexpected console error")).toThrow(/fatal browser signal/);
expect(diagnostics.browserSignals[0]).toEqual(
expect.objectContaining({ environmentalWarning: null, severity: "fatal", type: "console.error" }),
);
diagnostics.stop();
});
test("classifies only page console errors, never page exceptions or unhandled rejections", () => {
const details = {
location: { url: "https://rr2---sn-q4flrnel.googlevideo.com/videoplayback" },
message: "Failed to load resource: net::ERR_NAME_NOT_RESOLVED",
source: "page",
};
expect(environmentalConsoleWarningId({ ...details, type: "console.error" })).toBe("googlevideo-resource-failure");
expect(environmentalConsoleWarningId({ ...details, type: "pageerror" })).toBeNull();
expect(environmentalConsoleWarningId({ ...details, type: "unhandledrejection" })).toBeNull();
expect(
environmentalConsoleWarningId({ ...details, source: "extension-service-worker", type: "console.error" }),
).toBeNull();
});
test("fails closed if a future signal reaches the guard without an explicit severity", () => {
const { context, page } = createLiveHarness();
const guard = new LiveFatalSignalGuard(page, context, { log: jest.fn() });
guard.signals.push({ message: "unclassified", type: "console.error" });
expect(() => guard.assertNoFatalSignalsBetween(0, 1, "future signal")).toThrow(/console\.error: unclassified/);
});
test("allows only an explicit exact benign-console rule and keeps mismatches fatal", async () => {
const { diagnostics, page } = createLiveHarness({
ignoredSignalRules: [
{
id: "known-youtube-message",
message: "Known YouTube-only diagnostic",
type: "console.error",
url: "https://www.youtube.com/watch?v=abcdefghijk",
},
],
});
await diagnostics.start();
page.emit("console", consoleError("Known YouTube-only diagnostic"));
expect(() => diagnostics.consumeFatalSignals("known noise")).not.toThrow();
expect(diagnostics.browserSignals[0]).toEqual(
expect.objectContaining({ ignoredBy: "known-youtube-message", severity: "warning" }),
);
page.emit("console", consoleError("Known YouTube-only diagnostic with extra text"));
expect(() => diagnostics.consumeFatalSignals("unexpected noise")).toThrow(/fatal browser signal/);
diagnostics.stop();
});
test("rejects broad or callback-based console suppression rules", () => {
const { context, page } = createLiveHarness();
expect(
() =>
new LiveFatalSignalGuard(page, context, {
ignoredSignalRules: [{ id: "message-only", message: "Failed", type: "console.error" }],
}),
).toThrow(/requires a non-empty exact url/);
expect(
() =>
new LiveFatalSignalGuard(page, context, {
ignoredSignalRules: [{ id: "broad", messagePattern: /error/, type: "console.error" }],
}),
).toThrow(/unsupported fields: messagePattern/);
expect(
() =>
new LiveFatalSignalGuard(page, context, {
ignoredSignalRules: [{ id: "callback", message: "ignored", predicate: () => true, type: "console.error" }],
}),
).toThrow(/unsupported fields: predicate/);
});
test("fails only for console errors from the selected extension service worker", async () => {
const extensionId = "a".repeat(32);
const matchingWorker = new FakeEmitter();
matchingWorker.url = jest.fn(() => `chrome-extension://${extensionId}/ryd.background.js`);
matchingWorker.evaluate = jest.fn().mockResolvedValue(undefined);
const unrelatedWorker = new FakeEmitter();
unrelatedWorker.url = jest.fn(() => `chrome-extension://${"b".repeat(32)}/ryd.background.js`);
unrelatedWorker.evaluate = jest.fn().mockResolvedValue(undefined);
const { context, diagnostics } = createLiveHarness({ selectedExtensionId: extensionId });
context.serviceWorkers.mockReturnValue([matchingWorker, unrelatedWorker]);
await diagnostics.start();
unrelatedWorker.emit("console", consoleError("unrelated extension failed", unrelatedWorker.url()));
expect(() => diagnostics.consumeFatalSignals("unrelated worker")).not.toThrow();
matchingWorker.emit("console", consoleError("selected extension failed", matchingWorker.url()));
expect(() => diagnostics.consumeFatalSignals("selected worker")).toThrow(
/extension-service-worker\.console\.error: selected extension failed/,
);
expect(matchingWorker.evaluate).toHaveBeenCalledWith(expect.any(Function), UNHANDLED_REJECTION_PREFIX);
expect(unrelatedWorker.evaluate).not.toHaveBeenCalled();
diagnostics.stop();
});
test("keeps external-resource wording fatal when it comes from the selected extension worker", async () => {
const extensionId = "a".repeat(32);
const matchingWorker = new FakeEmitter();
matchingWorker.url = jest.fn(() => `chrome-extension://${extensionId}/ryd.background.js`);
matchingWorker.evaluate = jest.fn().mockResolvedValue(undefined);
const { context, diagnostics } = createLiveHarness({ selectedExtensionId: extensionId });
context.serviceWorkers.mockReturnValue([matchingWorker]);
await diagnostics.start();
matchingWorker.emit(
"console",
consoleError(
"Failed to load resource: net::ERR_NAME_NOT_RESOLVED",
"https://rr2---sn-q4flrnel.googlevideo.com/videoplayback",
),
);
expect(() => diagnostics.consumeFatalSignals("selected worker")).toThrow(
/extension-service-worker\.console\.error/,
);
expect(diagnostics.browserSignals[0]).toEqual(
expect.objectContaining({ environmentalWarning: null, severity: "fatal" }),
);
diagnostics.stop();
});
test("guards a selected extension service worker that restarts during a scenario", async () => {
const extensionId = "a".repeat(32);
const replacementWorker = new FakeEmitter();
replacementWorker.url = jest.fn(() => `chrome-extension://${extensionId}/ryd.background.js`);
replacementWorker.evaluate = jest.fn().mockResolvedValue(undefined);
const { context, diagnostics } = createLiveHarness({ selectedExtensionId: extensionId });
await diagnostics.start();
context.emit("serviceworker", replacementWorker);
replacementWorker.emit(
"console",
consoleError(`${UNHANDLED_REJECTION_PREFIX}replacement rejected`, replacementWorker.url()),
);
expect(() => diagnostics.consumeFatalSignals("replacement worker")).toThrow(
/extension-service-worker\.unhandledrejection: replacement rejected/,
);
diagnostics.stop();
});
test("continues independent read-only stages, then rejects the aggregate run", async () => {
const diagnostics = {
consumeFatalSignals: jest.fn(),
stageCompleted: jest.fn(),
stageFailed: jest.fn(),
stageStarted: jest.fn(),
};
const laterStage = jest.fn().mockResolvedValue("healthy");
let aggregate;
try {
await runIndependentLoggedStages(diagnostics, [
{
action: async () => {
throw new Error("first failed");
},
name: "first",
},
{ action: laterStage, name: "second" },
]);
} catch (error) {
aggregate = error;
}
expect(aggregate).toBeInstanceOf(AggregateError);
expect(aggregate.message).toBe("1 of 2 independent live read-only stages failed.");
expect(aggregate.stageResults).toEqual([
expect.objectContaining({ error: expect.objectContaining({ message: "first failed" }), name: "first" }),
{ name: "second", result: "healthy" },
]);
expect(laterStage).toHaveBeenCalledTimes(1);
expect(diagnostics.stageCompleted).toHaveBeenCalledWith("second", expect.any(Number));
});
test("blocks production reactions unless this worker completed every read-only scenario", () => {
const gate = new LiveReadOnlyGate(["shorts-render", "watch-render", "responsive-visual"], {
allowedSkippedScenarioIds: ["watch-render"],
});
gate.record("shorts-render", "passed");
gate.record("watch-render", "skipped");
expect(() => gate.assertPassed()).toThrow(/Missing: responsive-visual; failed: none/);
gate.record("responsive-visual", "passed");
expect(() => gate.assertPassed()).not.toThrow();
gate.record("shorts-render", "failed");
gate.record("shorts-render", "passed");
expect(() => gate.assertPassed()).toThrow(/Missing: shorts-render; failed: shorts-render/);
const requiredSkip = new LiveReadOnlyGate(["shorts-render"]);
requiredSkip.record("shorts-render", "skipped");
expect(() => requiredSkip.assertPassed()).toThrow(/failed: shorts-render/);
});
test("a fresh worker cannot inherit read-only gate success from a previous worker", () => {
const firstWorker = new LiveReadOnlyGate(["shorts-render"]);
firstWorker.record("shorts-render", "passed");
expect(() => firstWorker.assertPassed()).not.toThrow();
const restartedWorker = new LiveReadOnlyGate(["shorts-render"]);
expect(() => restartedWorker.assertPassed()).toThrow(/Missing: shorts-render/);
});
test("redacts identity and proof material while retaining diagnostic video IDs", () => {
expect(
diagnosticApiUrl(
@@ -0,0 +1,397 @@
const path = require("node:path");
const {
assertWorkerIdentity,
extensionServiceWorkerUrl,
normalizeExtensionPath,
readUnpackedExtensions,
reloadLiveExtension,
reloadLiveExtensionInBrowser,
selectEnabledUnpackedExtension,
waitForReloadedExtensionEnabled,
waitForExtensionWorker,
} = require("../e2e/live/live-extension-reloader");
const EXTENSION_ID = "abcdefghijklmnopabcdefghijklmnop";
const BUILD_ID = "0123456789abcdef0123456789abcdef";
const EXTENSION_PATH = "C:\\repo\\Extensions\\combined\\dist\\chrome";
function identity(overrides = {}) {
return {
backgroundScript: "ryd.background.js",
compiledBuildId: BUILD_ID,
extensionId: EXTENSION_ID,
resourceBuildId: BUILD_ID,
version: "4.0.5",
...overrides,
};
}
async function flushPromiseJobs() {
for (let index = 0; index < 10; index += 1) await Promise.resolve();
}
describe("live unpacked-extension reload helper", () => {
test("matches the exact unpacked path with Windows separator and case normalization", () => {
const extension = selectEnabledUnpackedExtension(
[
{ enabled: true, id: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", path: "C:\\other", version: "4.0.5" },
{ enabled: true, id: EXTENSION_ID, path: "c:/REPO/Extensions/combined/dist/chrome/", version: "4.0.4" },
],
EXTENSION_PATH,
{ pathImpl: path.win32, platform: "win32" },
);
expect(extension.id).toBe(EXTENSION_ID);
expect(normalizeExtensionPath(extension.path, { pathImpl: path.win32, platform: "win32" })).toBe(
normalizeExtensionPath(EXTENSION_PATH, { pathImpl: path.win32, platform: "win32" }),
);
});
test.each([
[[], /is not installed/],
[[{ enabled: false, id: EXTENSION_ID, path: EXTENSION_PATH }], /installed but disabled/],
[[{ enabled: true, id: "not-an-id", path: EXTENSION_PATH }], /invalid extension ID/],
[
[
{ enabled: true, id: EXTENSION_ID, path: EXTENSION_PATH },
{ enabled: true, id: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", path: EXTENSION_PATH },
],
/more than one/,
],
])("refuses an unsafe unpacked-extension inventory", (extensions, expectedError) => {
expect(() =>
selectEnabledUnpackedExtension(extensions, EXTENSION_PATH, {
pathImpl: path.win32,
platform: "win32",
}),
).toThrow(expectedError);
});
test("can use the default-profile inventory only for exact path-to-ID identification", () => {
expect(
selectEnabledUnpackedExtension(
[{ enabled: false, id: EXTENSION_ID, path: EXTENSION_PATH, version: "4.0.5" }],
EXTENSION_PATH,
{ pathImpl: path.win32, platform: "win32", requireEnabled: false },
),
).toMatchObject({ enabled: false, id: EXTENSION_ID });
});
test("uses an exact extension worker URL", () => {
expect(extensionServiceWorkerUrl(EXTENSION_ID)).toBe(`chrome-extension://${EXTENSION_ID}/ryd.background.js`);
expect(() => extensionServiceWorkerUrl("not-an-id")).toThrow(/valid Chrome extension ID/);
});
test("starts a dormant worker through the ServiceWorker domain", async () => {
const worker = { url: () => extensionServiceWorkerUrl(EXTENSION_ID) };
let workers = [];
const context = { serviceWorkers: jest.fn(() => workers) };
const serviceWorkerSession = {
send: jest.fn(async (method) => {
if (method === "ServiceWorker.startWorker") workers = [worker];
}),
};
await expect(
waitForExtensionWorker(context, serviceWorkerSession, EXTENSION_ID, {
delayImpl: async () => {},
now: (() => {
let value = 0;
return () => value++;
})(),
timeoutMilliseconds: 20,
}),
).resolves.toBe(worker);
expect(serviceWorkerSession.send).toHaveBeenNthCalledWith(1, "ServiceWorker.enable");
expect(serviceWorkerSession.send).toHaveBeenNthCalledWith(2, "ServiceWorker.startWorker", {
scopeURL: `chrome-extension://${EXTENSION_ID}/`,
});
});
test("fails closed when Chrome cannot inventory unpacked extensions", async () => {
const browserSession = { send: jest.fn().mockRejectedValue(new Error("Method not available")) };
await expect(readUnpackedExtensions(browserSession)).rejects.toThrow(
/cannot be identified safely.*no extension was changed/i,
);
});
test("polls through Chrome's transient post-reload disabled inventory state", async () => {
jest.useFakeTimers({ doNotFake: ["performance"] });
try {
const browserSession = {
send: jest
.fn()
.mockResolvedValueOnce({
extensions: [{ enabled: false, id: EXTENSION_ID, path: EXTENSION_PATH, version: "4.0.5" }],
})
.mockResolvedValueOnce({
extensions: [{ enabled: true, id: EXTENSION_ID, path: EXTENSION_PATH, version: "4.0.5" }],
}),
};
const extensionPromise = waitForReloadedExtensionEnabled(browserSession, EXTENSION_PATH, EXTENSION_ID, {
pathOptions: { pathImpl: path.win32, platform: "win32" },
timeoutMilliseconds: 100,
});
await flushPromiseJobs();
expect(jest.getTimerCount()).toBe(1);
jest.advanceTimersByTime(50);
await flushPromiseJobs();
await expect(extensionPromise).resolves.toMatchObject({ enabled: true, id: EXTENSION_ID });
expect(browserSession.send).toHaveBeenCalledTimes(2);
} finally {
jest.useRealTimers();
}
});
test("times out when Chrome's post-reload inventory remains disabled", async () => {
jest.useFakeTimers({ doNotFake: ["performance"] });
try {
const browserSession = {
send: jest.fn().mockResolvedValue({
extensions: [{ enabled: false, id: EXTENSION_ID, path: EXTENSION_PATH, version: "4.0.5" }],
}),
};
const extensionPromise = waitForReloadedExtensionEnabled(browserSession, EXTENSION_PATH, EXTENSION_ID, {
pathOptions: { pathImpl: path.win32, platform: "win32" },
timeoutMilliseconds: 100,
});
const rejection = expect(extensionPromise).rejects.toThrow(/remained disabled for 100ms after reload/);
await flushPromiseJobs();
expect(jest.getTimerCount()).toBe(1);
jest.advanceTimersByTime(50);
await flushPromiseJobs();
expect(jest.getTimerCount()).toBe(1);
jest.advanceTimersByTime(50);
await flushPromiseJobs();
await rejection;
expect(browserSession.send).toHaveBeenCalledTimes(3);
} finally {
jest.useRealTimers();
}
});
test("rejects a different extension ID immediately while polling the exact unpacked path", async () => {
const differentExtensionId = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
const browserSession = {
send: jest.fn().mockResolvedValue({
extensions: [{ enabled: false, id: differentExtensionId, path: EXTENSION_PATH, version: "4.0.5" }],
}),
};
await expect(
waitForReloadedExtensionEnabled(browserSession, EXTENSION_PATH, EXTENSION_ID, {
pathOptions: { pathImpl: path.win32, platform: "win32" },
timeoutMilliseconds: 100,
}),
).rejects.toThrow(`changed from ${EXTENSION_ID} to ${differentExtensionId}`);
expect(browserSession.send).toHaveBeenCalledTimes(1);
});
test("requires both the compiled and resource build IDs after reload", () => {
expect(() =>
assertWorkerIdentity(identity({ compiledBuildId: "f".repeat(32) }), EXTENSION_ID, {
expectedBuildId: BUILD_ID,
expectedVersion: "4.0.5",
}),
).toThrow(/is running build/);
expect(() =>
assertWorkerIdentity(identity({ resourceBuildId: "f".repeat(32) }), EXTENSION_ID, {
expectedBuildId: BUILD_ID,
expectedVersion: "4.0.5",
}),
).toThrow(/exposes resource build/);
});
test("reloads only the exact enabled unpacked extension and proves the replacement worker build", async () => {
let workers;
let inventoryVersion = "4.0.4";
let closeOldWorker;
const oldWorkerClosed = new Promise((resolve) => {
closeOldWorker = resolve;
});
const oldWorker = {
evaluate: jest
.fn()
.mockResolvedValueOnce(
identity({ compiledBuildId: "a".repeat(32), resourceBuildId: BUILD_ID, version: "4.0.4" }),
)
.mockImplementationOnce(async () => {
workers = [];
inventoryVersion = "4.0.5";
closeOldWorker();
return true;
}),
url: () => extensionServiceWorkerUrl(EXTENSION_ID),
waitForEvent: jest.fn(() => oldWorkerClosed),
};
const newWorker = {
evaluate: jest.fn().mockResolvedValue(identity()),
url: () => extensionServiceWorkerUrl(EXTENSION_ID),
};
workers = [oldWorker];
const browserSession = {
send: jest.fn(async (method) => {
expect(method).toBe("Extensions.getExtensions");
return {
extensions: [{ enabled: true, id: EXTENSION_ID, path: EXTENSION_PATH, version: inventoryVersion }],
};
}),
};
const context = { serviceWorkers: jest.fn(() => workers) };
const serviceWorkerSession = {
send: jest.fn(async (method) => {
if (method === "ServiceWorker.startWorker") workers = [newWorker];
}),
};
await expect(
reloadLiveExtension({
browserSession,
context,
expectedBuildId: BUILD_ID,
expectedExtensionPath: EXTENSION_PATH,
expectedVersion: "4.0.5",
pathOptions: { pathImpl: path.win32, platform: "win32" },
serviceWorkerSession,
timeoutMilliseconds: 100,
waitOptions: { delayImpl: async () => {}, now: Date.now },
}),
).resolves.toEqual({
buildId: BUILD_ID,
extensionId: EXTENSION_ID,
previousVersion: "4.0.4",
version: "4.0.5",
});
expect(browserSession.send).toHaveBeenCalledTimes(2);
expect(oldWorker.waitForEvent).toHaveBeenCalledWith("close", { timeout: 100 });
expect(oldWorker.evaluate).toHaveBeenCalledTimes(2);
expect(newWorker.evaluate).toHaveBeenCalledTimes(1);
});
test("refuses to start or reload the unpacked extension when Chrome reports it disabled", async () => {
const browserSession = {
send: jest.fn().mockResolvedValue({
extensions: [{ enabled: false, id: EXTENSION_ID, path: EXTENSION_PATH, version: "4.0.5" }],
}),
};
const context = { serviceWorkers: jest.fn() };
const serviceWorkerSession = { send: jest.fn() };
await expect(
reloadLiveExtension({
browserSession,
context,
expectedBuildId: BUILD_ID,
expectedExtensionPath: EXTENSION_PATH,
expectedVersion: "4.0.5",
pathOptions: { pathImpl: path.win32, platform: "win32" },
serviceWorkerSession,
}),
).rejects.toThrow(/installed but disabled/);
expect(context.serviceWorkers).not.toHaveBeenCalled();
expect(serviceWorkerSession.send).not.toHaveBeenCalled();
});
test("does not request a worker reload when the expected unpacked path is absent", async () => {
const browserSession = {
send: jest.fn().mockResolvedValue({
extensions: [{ enabled: true, id: EXTENSION_ID, path: "C:\\somewhere-else", version: "4.0.5" }],
}),
};
const context = { serviceWorkers: jest.fn() };
const serviceWorkerSession = { send: jest.fn() };
await expect(
reloadLiveExtension({
browserSession,
context,
expectedBuildId: BUILD_ID,
expectedExtensionPath: EXTENSION_PATH,
expectedVersion: "4.0.5",
pathOptions: { pathImpl: path.win32, platform: "win32" },
serviceWorkerSession,
}),
).rejects.toThrow(/is not installed/);
expect(context.serviceWorkers).not.toHaveBeenCalled();
expect(serviceWorkerSession.send).not.toHaveBeenCalled();
});
test("uses only the caller-selected YouTube page for the target-profile ServiceWorker session", async () => {
const existingInternalPage = { url: () => "chrome://extensions/" };
const selectedPage = {
context: jest.fn(),
isClosed: jest.fn().mockReturnValue(false),
url: jest.fn().mockReturnValue("https://www.youtube.com/watch?v=AAAAAAAAAAA"),
};
const browserSession = { detach: jest.fn().mockResolvedValue(undefined) };
const serviceWorkerSession = { detach: jest.fn().mockResolvedValue(undefined) };
const browser = { newBrowserCDPSession: jest.fn().mockResolvedValue(browserSession) };
const context = {
newCDPSession: jest.fn().mockResolvedValue(serviceWorkerSession),
newPage: jest.fn(),
pages: jest.fn().mockReturnValue([existingInternalPage]),
};
selectedPage.context.mockReturnValue(context);
const reloadResult = { extensionId: EXTENSION_ID };
const reloadImpl = jest.fn().mockResolvedValue(reloadResult);
await expect(
reloadLiveExtensionInBrowser({
browser,
context,
expectedBuildId: BUILD_ID,
expectedExtensionPath: EXTENSION_PATH,
expectedVersion: "4.0.5",
reloadImpl,
sessionPage: selectedPage,
timeoutMilliseconds: 12345,
}),
).resolves.toBe(reloadResult);
expect(context.pages).not.toHaveBeenCalled();
expect(context.newPage).not.toHaveBeenCalled();
expect(context.newCDPSession).toHaveBeenCalledWith(selectedPage);
expect(reloadImpl).toHaveBeenCalledWith({
browserSession,
context,
expectedBuildId: BUILD_ID,
expectedExtensionPath: EXTENSION_PATH,
expectedVersion: "4.0.5",
serviceWorkerSession,
timeoutMilliseconds: 12345,
});
expect(serviceWorkerSession.detach).toHaveBeenCalledTimes(1);
expect(browserSession.detach).toHaveBeenCalledTimes(1);
});
test("rejects a session page from a different profile before opening a browser CDP session", async () => {
const otherContext = {};
const sessionPage = {
context: () => otherContext,
isClosed: () => false,
url: () => "https://www.youtube.com/",
};
const browser = { newBrowserCDPSession: jest.fn() };
const context = { newCDPSession: jest.fn(), newPage: jest.fn() };
await expect(
reloadLiveExtensionInBrowser({
browser,
context,
expectedBuildId: BUILD_ID,
expectedExtensionPath: EXTENSION_PATH,
expectedVersion: "4.0.5",
sessionPage,
}),
).rejects.toThrow(/does not belong to the attached Chrome context/);
expect(browser.newBrowserCDPSession).not.toHaveBeenCalled();
expect(context.newCDPSession).not.toHaveBeenCalled();
});
});
@@ -0,0 +1,137 @@
/**
* @jest-environment node
*/
const fs = require("node:fs");
const path = require("node:path");
const packageManifest = require("../../../package.json");
const {
LiveValidationIncompleteError,
REQUIRED_PRODUCTION_REACTION_SCENARIOS,
assertFullLiveReactionCompletion,
buildLiveRuntimeArtifact,
readFullLiveRuntime,
requireProductionReactionApproval,
requireUserscriptInstallAcknowledgement,
} = require("../e2e/live/live-full-run-contract");
test.each(["extension", "userscript"])("selects and builds the exact %s live runtime before a full run", (runtime) => {
const spawnSyncImpl = jest.fn(() => ({ status: 0 }));
const environment = { npm_execpath: "C:\\node\\node_modules\\npm\\bin\\npm-cli.js", RYD_LIVE_RUNTIME: runtime };
expect(readFullLiveRuntime(environment)).toBe(runtime);
expect(
buildLiveRuntimeArtifact(runtime, {
environment,
nodeExecutable: "C:\\node\\node.exe",
platform: "win32",
repositoryRoot: "repository",
spawnSyncImpl,
}),
).toEqual({ runtime, script: `build:live:${runtime}` });
expect(spawnSyncImpl).toHaveBeenCalledWith(
"C:\\node\\node.exe",
["C:\\node\\node_modules\\npm\\bin\\npm-cli.js", "run", `build:live:${runtime}`],
{
cwd: "repository",
env: environment,
stdio: "inherit",
},
);
});
test("uses a Windows command shell only when the npm CLI path is unavailable", () => {
const spawnSyncImpl = jest.fn(() => ({ status: 0 }));
const environment = { RYD_LIVE_RUNTIME: "extension" };
buildLiveRuntimeArtifact("extension", {
environment,
platform: "win32",
repositoryRoot: "repository",
spawnSyncImpl,
});
expect(spawnSyncImpl).toHaveBeenCalledWith("npm.cmd", ["run", "build:live:extension"], {
cwd: "repository",
env: environment,
shell: true,
stdio: "inherit",
});
});
test("a failed live build prevents the full runner from continuing", () => {
expect(() =>
buildLiveRuntimeArtifact("extension", {
spawnSyncImpl: () => ({ status: 2 }),
}),
).toThrow("build:live:extension failed with exit code 2");
});
test.each([undefined, "", "browser"])("rejects unsupported full-live runtime %p", (runtime) => {
expect(() => readFullLiveRuntime({ RYD_LIVE_RUNTIME: runtime })).toThrow(/RYD_LIVE_RUNTIME/);
});
test("classifies only both completed production reaction scenarios as a full run", () => {
expect(REQUIRED_PRODUCTION_REACTION_SCENARIOS).toEqual(["post-navigation-vote", "reaction-matrix"]);
expect(
assertFullLiveReactionCompletion({
"post-navigation-vote": { videoId: "abcdefghijk" },
"reaction-matrix": [{ action: "like" }],
}),
).toEqual({
classification: "full",
completedScenarioIds: REQUIRED_PRODUCTION_REACTION_SCENARIOS,
productionReactionsCompleted: true,
releaseReady: true,
});
});
test.each([
[null, /no production reaction results/],
[{}, /post-navigation-vote, reaction-matrix/],
[{ "post-navigation-vote": {} }, /reaction-matrix/],
[{ "reaction-matrix": [] }, /post-navigation-vote/],
[{ "post-navigation-vote": null, "reaction-matrix": {} }, /post-navigation-vote/],
])("rejects incomplete full-live reaction results %#", (results, message) => {
expect(() => assertFullLiveReactionCompletion(results)).toThrow(message);
});
test("SKIP is an explicit incomplete failure, not a successful full run", () => {
expect(() => requireProductionReactionApproval("SKIP")).toThrow(LiveValidationIncompleteError);
expect(() => requireProductionReactionApproval(" skip ")).toThrow(/production reaction scenarios were skipped/);
expect(requireProductionReactionApproval("extension:abcdefghijk:123")).toBe("extension:abcdefghijk:123");
});
test("a freshly built userscript requires an explicit installation acknowledgement", () => {
expect(requireUserscriptInstallAcknowledgement("INSTALLED")).toBe(true);
expect(() => requireUserscriptInstallAcknowledgement("SKIP")).toThrow(LiveValidationIncompleteError);
expect(() => requireUserscriptInstallAcknowledgement("")).toThrow(/not acknowledged as installed/);
});
test("package commands distinguish full reaction validation from the read-only Playwright suite", () => {
expect(packageManifest.scripts["test:live:youtube"]).toBe(
"node Extensions/UserScript/e2e/live/live-interactive-runner.js",
);
expect(packageManifest.scripts["test:live:youtube:full"]).toBe("npm run test:live:youtube");
expect(packageManifest.scripts["test:live:youtube:interactive"]).toBe("npm run test:live:youtube");
expect(packageManifest.scripts["test:live:youtube:read-only"]).toBe(
"playwright test --config playwright.live-youtube.config.js",
);
});
test("the read-only Playwright suite cannot silently register a skipped reaction test", () => {
const source = fs.readFileSync(path.resolve(__dirname, "../e2e/live/live-youtube.live.e2e.js"), "utf8");
expect(source).toContain('test.describe("live YouTube RYD read-only smoke"');
expect(source).not.toMatch(/\btest\.skip\s*\(/);
expect(source).not.toContain("reaction-matrix");
expect(source).not.toContain("post-navigation-vote");
expect(source).toContain("LIVE_VALIDATION_READ_ONLY_COMPLETE");
expect(source).toContain("releaseReady: false");
});
test("standalone artifact runners verify current source before consuming existing output", () => {
expect(packageManifest.scripts["test:e2e:userscript:run"]).toMatch(/^npm run check:userscript-artifact && /);
expect(packageManifest.scripts["test:e2e:artifacts"]).toMatch(
/^npm run check:extension-artifact && npm run check:userscript-artifact && /,
);
});
@@ -0,0 +1,212 @@
const {
INTERACTIVE_READ_ONLY_SCENARIOS,
createInteractiveReadOnlyStages,
runInteractiveReadOnlyAfterRuntimeReload,
} = require("../e2e/live/live-interactive-readonly");
const {
REQUIRED_INTERACTIVE_READ_ONLY_SCENARIO_IDS,
assertInteractiveReadOnlyScenarioCompletions,
assertInteractiveReadOnlyScenarioDefinitions,
} = require("../e2e/live/live-interactive-scenario-contract");
const OPTIONS = Object.freeze({
navigation: Object.freeze({ watch: "watch-target" }),
runtime: "extension",
short: "9LjMX9xeeok",
});
describe("interactive live read-only ordering", () => {
test("declares the exact independent required scenario set with the cold Short first", () => {
expect(REQUIRED_INTERACTIVE_READ_ONLY_SCENARIO_IDS).toEqual([
"shorts-render",
"channel-shorts-navigation",
"channel-watch-navigation",
"reload",
"responsive-visual",
"spa-navigation",
"watch-action-topology",
"sidebar-navigation-stress",
"watch-render",
]);
expect(new Set(REQUIRED_INTERACTIVE_READ_ONLY_SCENARIO_IDS)).toHaveProperty(
"size",
REQUIRED_INTERACTIVE_READ_ONLY_SCENARIO_IDS.length,
);
expect(REQUIRED_INTERACTIVE_READ_ONLY_SCENARIO_IDS[0]).toBe("shorts-render");
});
test("keeps the implementation aligned with the independent required order", () => {
const stages = createInteractiveReadOnlyStages({
diagnostics: { checkpoint: jest.fn() },
options: OPTIONS,
readOnly: {},
runtimeAdapter: {},
scenarioRunner: { run: jest.fn() },
});
expect(stages.map(({ scenarioId }) => scenarioId)).toEqual(REQUIRED_INTERACTIVE_READ_ONLY_SCENARIO_IDS);
expect(() => assertInteractiveReadOnlyScenarioDefinitions(INTERACTIVE_READ_ONLY_SCENARIOS)).not.toThrow();
expect(stages[0]).toMatchObject({ name: "read-only.short-render", scenarioId: "shorts-render" });
expect(stages.findIndex(({ scenarioId }) => scenarioId === "shorts-render")).toBeLessThan(
stages.findIndex(({ scenarioId }) => scenarioId === "channel-shorts-navigation"),
);
expect(stages.findIndex(({ scenarioId }) => scenarioId === "shorts-render")).toBeLessThan(
stages.findIndex(({ scenarioId }) => scenarioId === "channel-watch-navigation"),
);
});
test.each(REQUIRED_INTERACTIVE_READ_ONLY_SCENARIO_IDS)(
"fails the independent contract when %s is deleted from the implementation",
(deletedScenarioId) => {
const incompleteDefinitions = INTERACTIVE_READ_ONLY_SCENARIOS.filter(
({ scenarioId }) => scenarioId !== deletedScenarioId,
);
expect(() => assertInteractiveReadOnlyScenarioDefinitions(incompleteDefinitions)).toThrow(
new RegExp(`Missing: .*${deletedScenarioId}`),
);
},
);
test("rejects duplicate, unexpected, and reordered implementation scenarios", () => {
const duplicate = [
...INTERACTIVE_READ_ONLY_SCENARIOS,
{ key: "duplicate", name: "read-only.duplicate", scenarioId: "shorts-render" },
];
const unexpected = [
...INTERACTIVE_READ_ONLY_SCENARIOS.slice(0, -1),
{ key: "unexpected", name: "read-only.unexpected", scenarioId: "unexpected" },
];
const reordered = [...INTERACTIVE_READ_ONLY_SCENARIOS];
[reordered[0], reordered[1]] = [reordered[1], reordered[0]];
expect(() => assertInteractiveReadOnlyScenarioDefinitions(duplicate)).toThrow(/duplicate scenario IDs/);
expect(() => assertInteractiveReadOnlyScenarioDefinitions(unexpected)).toThrow(
/Missing: watch-render; unexpected: unexpected/,
);
expect(() => assertInteractiveReadOnlyScenarioDefinitions(reordered)).toThrow(
/required interactive read-only order/,
);
});
test("waits for the exact extension reload, then runs the configured Short cold before other scenarios", async () => {
const events = [];
let finishReload;
const reloadPending = new Promise((resolve) => {
finishReload = resolve;
});
const scenarioRunner = {
run: jest.fn(async (_adapter, scenarioId, receivedOptions) => {
events.push(`scenario:${scenarioId}:${receivedOptions.short}`);
return scenarioId;
}),
};
const execution = runInteractiveReadOnlyAfterRuntimeReload({
initializeRuntime: async (reloadResult) => {
events.push(`initialize:${reloadResult.extensionId}`);
return { diagnostics: {}, runtimeAdapter: { runtime: "extension" } };
},
options: OPTIONS,
reloadExtension: async () => {
events.push("reload:start");
const result = await reloadPending;
events.push(`reload:complete:${result.extensionId}`);
return result;
},
runStages: async (_diagnostics, stages) => {
for (const stage of stages) await stage.action();
},
scenarioRunner,
});
await Promise.resolve();
expect(events).toEqual(["reload:start"]);
finishReload({ extensionId: "installed-extension" });
await expect(execution).resolves.toMatchObject({
readOnly: { short: "shorts-render" },
reloadResult: { extensionId: "installed-extension" },
});
expect(events.slice(0, 4)).toEqual([
"reload:start",
"reload:complete:installed-extension",
"initialize:installed-extension",
`scenario:shorts-render:${OPTIONS.short}`,
]);
expect(scenarioRunner.run).toHaveBeenNthCalledWith(1, { runtime: "extension" }, "shorts-render", OPTIONS);
});
test("refuses to classify an unconfigured channel-to-Watch scenario as completed", () => {
expect(() =>
createInteractiveReadOnlyStages({
diagnostics: { checkpoint: jest.fn() },
options: { ...OPTIONS, navigation: { watch: null } },
readOnly: {},
runtimeAdapter: {},
scenarioRunner: { run: jest.fn() },
}),
).toThrow(/requires a configured channel Watch target/);
});
test("refuses reaction readiness when the stage executor silently omits any scenario", async () => {
const scenarioRunner = { run: jest.fn(async (_adapter, scenarioId) => scenarioId) };
await expect(
runInteractiveReadOnlyAfterRuntimeReload({
initializeRuntime: async () => ({ diagnostics: {}, runtimeAdapter: {} }),
options: OPTIONS,
reloadExtension: async () => ({ extensionId: "installed-extension" }),
runStages: async (_diagnostics, stages) => {
for (const stage of stages.slice(0, -1)) await stage.action();
},
scenarioRunner,
}),
).rejects.toThrow(/Missing: watch-render/);
expect(scenarioRunner.run).toHaveBeenCalledTimes(REQUIRED_INTERACTIVE_READ_ONLY_SCENARIO_IDS.length - 1);
});
test("refuses reaction readiness when a scenario resolves without a result", async () => {
await expect(
runInteractiveReadOnlyAfterRuntimeReload({
initializeRuntime: async () => ({ diagnostics: {}, runtimeAdapter: {} }),
options: OPTIONS,
reloadExtension: async () => ({ extensionId: "installed-extension" }),
runStages: async (_diagnostics, stages) => {
await stages[0].action();
},
scenarioRunner: { run: jest.fn().mockResolvedValue(undefined) },
}),
).rejects.toThrow(/shorts-render completed without a result/);
});
test("refuses reaction readiness when stages complete out of order or more than once", async () => {
const execute = (runStages) =>
runInteractiveReadOnlyAfterRuntimeReload({
initializeRuntime: async () => ({ diagnostics: {}, runtimeAdapter: {} }),
options: OPTIONS,
reloadExtension: async () => ({ extensionId: "installed-extension" }),
runStages,
scenarioRunner: { run: async (_adapter, scenarioId) => scenarioId },
});
await expect(
execute(async (_diagnostics, stages) => {
for (const stage of [...stages].reverse()) await stage.action();
}),
).rejects.toThrow(/required interactive read-only order/);
await expect(
execute(async (_diagnostics, stages) => {
for (const stage of stages) await stage.action();
await stages[0].action();
}),
).rejects.toThrow(/duplicate scenario IDs/);
});
test("completion validation independently rejects an empty or incomplete result set", () => {
expect(() => assertInteractiveReadOnlyScenarioCompletions([])).toThrow(/Missing:/);
expect(() =>
assertInteractiveReadOnlyScenarioCompletions(REQUIRED_INTERACTIVE_READ_ONLY_SCENARIO_IDS.slice(0, -1)),
).toThrow(/Missing: watch-render/);
});
});
@@ -0,0 +1,452 @@
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const { PassThrough } = require("node:stream");
const {
assertReusableConnectionOptions,
closeLiveAttemptPage,
createInputLineReader,
createInteractiveLiveDriver,
createInteractiveVoteRecorderFactory,
createReactionTargetDetails,
formatReactionApprovalReadiness,
main,
normalizeLiveSessionCommand,
preserveLiveAttemptEvidence,
readPersistentSessionMode,
resetLiveAttemptEvidenceWorkspace,
resolveSafeLiveEvidenceDirectory,
} = require("../e2e/live/live-interactive-runner");
const SELECTED_EXTENSION_ID = "abcdefghijklmnopabcdefghijklmnop";
const EXPECTED_BUILD_ID = "0123456789abcdef0123456789abcdef";
function createOutput() {
return { error: jest.fn(), log: jest.fn() };
}
function createLiveOptions(overrides = {}) {
return {
cdpConnectTimeoutMilliseconds: 120_000,
cdpEndpoint: "ws://127.0.0.1:60011/devtools/browser/session",
expectedBuildId: EXPECTED_BUILD_ID,
expectedChannel: "@anarios-ryd",
runtime: "extension",
...overrides,
};
}
function createContext() {
return {
off: jest.fn(),
on: jest.fn(),
pages: jest.fn(() => []),
};
}
function createPage() {
return {
setDefaultNavigationTimeout: jest.fn(),
setDefaultTimeout: jest.fn(),
};
}
describe("interactive live runtime binding", () => {
test("reports the dedicated reaction targets without conflating the read-only Short", () => {
const options = {
reactionShort: "reactshort1",
runtime: "extension",
short: "readonly001",
watchB: "reaction001",
};
expect(createReactionTargetDetails(options)).toEqual({
reactionShort: "reactshort1",
reactionWatch: "reaction001",
});
expect(formatReactionApprovalReadiness(options)).toBe(
"READY_FOR_REACTION_APPROVAL runtime=extension watch=reaction001 reactionShort=reactshort1 readOnlyShort=readonly001",
);
});
test("passes the selected account and exact extension ID into the live driver", () => {
const context = createContext();
const diagnostics = { checkpoint: jest.fn() };
const page = createPage();
const driver = createInteractiveLiveDriver({
context,
diagnostics,
options: { expectedBuildId: EXPECTED_BUILD_ID, expectedChannel: "@Expected.Handle", runtime: "extension" },
page,
selectedExtensionId: SELECTED_EXTENSION_ID,
});
expect(driver.authenticatedHandle).toBe("@expected.handle");
expect(driver.expectedBuildId).toBe(EXPECTED_BUILD_ID);
expect(driver.selectedExtensionId).toBe(SELECTED_EXTENSION_ID);
expect(() => driver.configureRequestAttributionRuntime("extension")).not.toThrow();
});
test("passes the same exact extension ID and selected page into every vote recorder", () => {
const context = createContext();
const page = createPage();
const createRecorder = createInteractiveVoteRecorderFactory({
context,
driver: { trafficLedger: null },
options: { runtime: "extension" },
page,
selectedExtensionId: SELECTED_EXTENSION_ID,
});
const recorder = createRecorder("abcdefghijk");
expect(recorder.page).toBe(page);
expect(recorder.runtime).toBe("extension");
expect(recorder.selectedExtensionId).toBe(SELECTED_EXTENSION_ID);
expect(recorder.videoId).toBe("abcdefghijk");
recorder.stop();
});
test("cannot create an extension vote recorder without the selected extension ID", () => {
const createRecorder = createInteractiveVoteRecorderFactory({
context: createContext(),
driver: { trafficLedger: null },
options: { runtime: "extension" },
page: createPage(),
selectedExtensionId: null,
});
expect(() => createRecorder("abcdefghijk")).toThrow(/requires the exact selected extension ID/);
});
test("keeps userscript recorders page-bound without inventing an extension identity", () => {
const context = createContext();
const page = createPage();
const createRecorder = createInteractiveVoteRecorderFactory({
context,
driver: { trafficLedger: null },
options: { runtime: "userscript" },
page,
selectedExtensionId: null,
});
const recorder = createRecorder("abcdefghijk");
expect(recorder.page).toBe(page);
expect(recorder.runtime).toBe("userscript");
expect(recorder.selectedExtensionId).toBeNull();
recorder.stop();
});
});
describe("interactive live attempt cleanup", () => {
test("closes a live-attempt page within the configured bound", async () => {
const page = {
close: jest.fn().mockResolvedValue(undefined),
isClosed: jest.fn(() => false),
};
await expect(closeLiveAttemptPage(page, { timeoutMs: 50 })).resolves.toBe(true);
expect(page.close).toHaveBeenCalledTimes(1);
});
test("does not try to close an already-closed live-attempt page", async () => {
const page = {
close: jest.fn(),
isClosed: jest.fn(() => true),
};
await expect(closeLiveAttemptPage(page, { timeoutMs: 50 })).resolves.toBe(false);
expect(page.close).not.toHaveBeenCalled();
});
test("rejects within the configured bound when page close never settles", async () => {
const page = {
close: jest.fn(() => new Promise(() => {})),
isClosed: jest.fn(() => false),
};
await expect(closeLiveAttemptPage(page, { timeoutMs: 10 })).rejects.toThrow(
"Timed out after 10ms while closing the live-attempt test page.",
);
});
});
describe("persistent interactive live session", () => {
test("buffers multiple control lines on one stdin reader for the whole connected process", async () => {
const input = new PassThrough();
const reader = createInputLineReader(input);
input.write("first\nRERUN\n");
await expect(reader.readLine()).resolves.toBe("first");
await expect(reader.readLine()).resolves.toBe("RERUN");
input.end();
await expect(reader.readLine()).resolves.toBeNull();
reader.close();
});
test.each([
[{ RYD_LIVE_RUNTIME: "extension" }, false],
[{ RYD_LIVE_KEEP_CDP_SESSION: "", RYD_LIVE_RUNTIME: "extension" }, false],
[{ RYD_LIVE_KEEP_CDP_SESSION: "1", RYD_LIVE_RUNTIME: "extension" }, true],
])("reads the opt-in without changing default behavior", (environment, expected) => {
expect(readPersistentSessionMode(environment)).toBe(expected);
});
test.each(["true", "yes", "0", "2"])("rejects invalid persistent-session value %s", (configured) => {
expect(() => readPersistentSessionMode({ RYD_LIVE_KEEP_CDP_SESSION: configured })).toThrow(/must be 1 or unset/);
});
test.each([
["RERUN", "RERUN"],
[" rerun ", "RERUN"],
["exit", "EXIT"],
[null, "EXIT"],
["", null],
["again", null],
])("normalizes persistent-session command %p", (value, expected) => {
expect(normalizeLiveSessionCommand(value)).toBe(expected);
});
test("keeps one browser connection across a failed attempt and a complete rerun", async () => {
const firstOptions = createLiveOptions();
const secondOptions = createLiveOptions({ expectedBuildId: "fedcba9876543210fedcba9876543210" });
const browser = { close: jest.fn().mockResolvedValue(undefined) };
const selectedSession = { context: {}, contextIndex: 0, pageIndex: 1, sessionPage: {} };
const buildArtifact = jest
.fn()
.mockReturnValueOnce({ runtime: "extension", script: "build:live:extension", attempt: 1 })
.mockReturnValueOnce({ runtime: "extension", script: "build:live:extension", attempt: 2 });
const connectBrowser = jest.fn().mockResolvedValue(browser);
const executeAttempt = jest
.fn()
.mockRejectedValueOnce(new Error("first live attempt failed"))
.mockResolvedValueOnce({ classification: "full", attempt: 2 });
const output = createOutput();
const preserveEvidence = jest
.fn()
.mockResolvedValueOnce("attempt-01-failed")
.mockResolvedValueOnce("attempt-02-passed");
const resetEvidenceWorkspace = jest
.fn()
.mockResolvedValueOnce({ removedEntries: ["stale-first"] })
.mockResolvedValueOnce({ removedEntries: ["stale-second"] });
const readLine = jest.fn().mockResolvedValueOnce("RERUN").mockResolvedValueOnce("EXIT");
const readOptions = jest.fn().mockReturnValueOnce(firstOptions).mockReturnValueOnce(secondOptions);
const selectContext = jest.fn().mockResolvedValue(selectedSession);
await expect(
main({
buildArtifact,
connectBrowser,
environment: { RYD_LIVE_KEEP_CDP_SESSION: "1", RYD_LIVE_RUNTIME: "extension" },
executeAttempt,
output,
preserveEvidence,
readLine,
readOptions,
resetEvidenceWorkspace,
selectContext,
}),
).resolves.toEqual({ classification: "full", attempt: 2 });
expect(buildArtifact).toHaveBeenCalledTimes(2);
expect(readOptions).toHaveBeenCalledTimes(2);
expect(connectBrowser).toHaveBeenCalledTimes(1);
expect(selectContext).toHaveBeenCalledTimes(1);
expect(executeAttempt).toHaveBeenCalledTimes(2);
expect(resetEvidenceWorkspace).toHaveBeenNthCalledWith(1, { attemptNumber: 1 });
expect(resetEvidenceWorkspace).toHaveBeenNthCalledWith(2, { attemptNumber: 2 });
expect(resetEvidenceWorkspace.mock.invocationCallOrder[0]).toBeLessThan(executeAttempt.mock.invocationCallOrder[0]);
expect(preserveEvidence.mock.invocationCallOrder[0]).toBeLessThan(
resetEvidenceWorkspace.mock.invocationCallOrder[1],
);
expect(resetEvidenceWorkspace.mock.invocationCallOrder[1]).toBeLessThan(executeAttempt.mock.invocationCallOrder[1]);
expect(executeAttempt.mock.calls[0][0]).toMatchObject({ browser, options: firstOptions, selectedSession });
expect(executeAttempt.mock.calls[1][0]).toMatchObject({ browser, options: secondOptions, selectedSession });
expect(preserveEvidence).toHaveBeenNthCalledWith(1, { attemptNumber: 1, outcome: "failed" });
expect(preserveEvidence).toHaveBeenNthCalledWith(2, { attemptNumber: 2, outcome: "passed" });
expect(output.log).toHaveBeenCalledWith("READY_FOR_LIVE_RERUN_OR_EXIT attempt=1 outcome=failed");
expect(output.log).toHaveBeenCalledWith("READY_FOR_LIVE_RERUN_OR_EXIT attempt=2 outcome=passed");
expect(browser.close).toHaveBeenCalledTimes(1);
});
test("retains normal one-attempt exit and failure semantics when persistence is unset", async () => {
const options = createLiveOptions();
const browser = { close: jest.fn().mockResolvedValue(undefined) };
const failure = new Error("strict live failure");
const executeAttempt = jest.fn().mockRejectedValue(failure);
const preserveEvidence = jest.fn();
const readLine = jest.fn();
await expect(
main({
buildArtifact: jest.fn(() => ({ runtime: "extension" })),
connectBrowser: jest.fn().mockResolvedValue(browser),
environment: { RYD_LIVE_RUNTIME: "extension" },
executeAttempt,
output: createOutput(),
preserveEvidence,
readLine,
readOptions: jest.fn(() => options),
resetEvidenceWorkspace: jest.fn().mockResolvedValue({ removedEntries: [] }),
selectContext: jest.fn(async () => ({ context: {}, contextIndex: 0, pageIndex: 0, sessionPage: {} })),
}),
).rejects.toBe(failure);
expect(executeAttempt).toHaveBeenCalledTimes(1);
expect(preserveEvidence).not.toHaveBeenCalled();
expect(readLine).not.toHaveBeenCalled();
expect(browser.close).toHaveBeenCalledTimes(1);
});
test("returns the latest failed attempt as a failure when a persistent session exits", async () => {
const options = createLiveOptions();
const browser = { close: jest.fn().mockResolvedValue(undefined) };
const failure = new Error("persistent live failure");
await expect(
main({
buildArtifact: jest.fn(() => ({ runtime: "extension" })),
connectBrowser: jest.fn().mockResolvedValue(browser),
environment: { RYD_LIVE_KEEP_CDP_SESSION: "1", RYD_LIVE_RUNTIME: "extension" },
executeAttempt: jest.fn().mockRejectedValue(failure),
output: createOutput(),
preserveEvidence: jest.fn().mockResolvedValue("attempt-01-failed"),
readLine: jest.fn().mockResolvedValue("EXIT"),
readOptions: jest.fn(() => options),
resetEvidenceWorkspace: jest.fn().mockResolvedValue({ removedEntries: [] }),
selectContext: jest.fn(async () => ({ context: {}, contextIndex: 0, pageIndex: 0, sessionPage: {} })),
}),
).rejects.toBe(failure);
expect(browser.close).toHaveBeenCalledTimes(1);
});
test("keeps the connection open while rejecting unknown rerun commands", async () => {
const options = createLiveOptions();
const browser = { close: jest.fn().mockResolvedValue(undefined) };
const output = createOutput();
const executeAttempt = jest.fn().mockResolvedValue({ classification: "full" });
const readLine = jest
.fn()
.mockResolvedValueOnce("not-a-command")
.mockResolvedValueOnce("RERUN")
.mockResolvedValueOnce("EXIT");
await main({
buildArtifact: jest.fn(() => ({ runtime: "extension" })),
connectBrowser: jest.fn().mockResolvedValue(browser),
environment: { RYD_LIVE_KEEP_CDP_SESSION: "1", RYD_LIVE_RUNTIME: "extension" },
executeAttempt,
output,
preserveEvidence: jest.fn().mockResolvedValue("evidence"),
readLine,
readOptions: jest.fn(() => options),
resetEvidenceWorkspace: jest.fn().mockResolvedValue({ removedEntries: [] }),
selectContext: jest.fn(async () => ({ context: {}, contextIndex: 0, pageIndex: 0, sessionPage: {} })),
});
expect(output.error).toHaveBeenCalledWith("LIVE_RERUN_COMMAND_REJECTED expected=RERUN_OR_EXIT");
expect(executeAttempt).toHaveBeenCalledTimes(2);
expect(browser.close).toHaveBeenCalledTimes(1);
});
test("fails closed if a rerun tries to switch endpoint, account, or runtime", () => {
const initial = createLiveOptions();
expect(() => assertReusableConnectionOptions(initial, { ...initial, cdpEndpoint: "ws://other" })).toThrow(
/cdpEndpoint changed/,
);
expect(() => assertReusableConnectionOptions(initial, { ...initial, expectedChannel: "@other" })).toThrow(
/expectedChannel changed/,
);
expect(() => assertReusableConnectionOptions(initial, { ...initial, runtime: "userscript" })).toThrow(
/runtime changed/,
);
expect(assertReusableConnectionOptions(initial, { ...initial, expectedBuildId: "f".repeat(32) })).toMatchObject({
expectedBuildId: "f".repeat(32),
});
});
test("copies each persistent attempt's evidence without recursively copying prior attempt archives", () => {
const evidenceDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "ryd-live-attempt-"));
try {
fs.mkdirSync(path.join(evidenceDirectory, "responsive"), { recursive: true });
fs.writeFileSync(path.join(evidenceDirectory, "responsive", "watch.png"), "image");
fs.mkdirSync(path.join(evidenceDirectory, "attempts", "old"), { recursive: true });
fs.writeFileSync(path.join(evidenceDirectory, "attempts", "old", "old.json"), "{}");
const fixedDate = new Date("2026-09-04T12:00:00.000Z");
const archived = preserveLiveAttemptEvidence(
{ attemptNumber: 3, outcome: "failed" },
{ evidenceDirectory, now: () => fixedDate },
);
expect(archived).toBe(path.join(evidenceDirectory, "attempts", "03-2026-09-04T12-00-00-000Z-failed"));
expect(fs.readFileSync(path.join(archived, "responsive", "watch.png"), "utf8")).toBe("image");
expect(fs.existsSync(path.join(archived, "attempts", "old", "old.json"))).toBe(false);
expect(JSON.parse(fs.readFileSync(path.join(archived, "attempt.json"), "utf8"))).toEqual({
attemptNumber: 3,
capturedAt: fixedDate.toISOString(),
outcome: "failed",
});
} finally {
fs.rmSync(evidenceDirectory, { force: true, recursive: true });
}
});
test("removes only the active attempt workspace while preserving every prior attempt archive", () => {
const temporaryRoot = fs.mkdtempSync(path.join(os.tmpdir(), "ryd-live-reset-"));
const evidenceDirectory = path.join(temporaryRoot, "live-youtube");
try {
fs.mkdirSync(path.join(evidenceDirectory, "responsive"), { recursive: true });
fs.writeFileSync(path.join(evidenceDirectory, "responsive", "stale-watch.png"), "stale image");
fs.writeFileSync(path.join(evidenceDirectory, "manual-persistent.png"), "stale manual image");
fs.mkdirSync(path.join(evidenceDirectory, "Attempts", "01-passed"), { recursive: true });
fs.writeFileSync(path.join(evidenceDirectory, "Attempts", "01-passed", "attempt.json"), "{}");
const reset = resetLiveAttemptEvidenceWorkspace(
{ attemptNumber: 2 },
{ allowedParentDirectory: temporaryRoot, evidenceDirectory },
);
expect(reset).toEqual({
attemptNumber: 2,
evidenceDirectory: fs.realpathSync.native(evidenceDirectory),
removedEntries: ["manual-persistent.png", "responsive"],
});
expect(fs.readdirSync(evidenceDirectory).map((entry) => entry.toLowerCase())).toEqual(["attempts"]);
expect(fs.readFileSync(path.join(evidenceDirectory, "Attempts", "01-passed", "attempt.json"), "utf8")).toBe("{}");
} finally {
fs.rmSync(temporaryRoot, { force: true, recursive: true });
}
});
test("fails closed for filesystem roots, the repository root, and broader evidence parents", () => {
const repositoryDirectory = path.resolve(__dirname, "../../..");
const filesystemRoot = path.parse(repositoryDirectory).root;
expect(() => resolveSafeLiveEvidenceDirectory(filesystemRoot)).toThrow(/unsafe live evidence directory/);
expect(() => resolveSafeLiveEvidenceDirectory(repositoryDirectory)).toThrow(/unsafe live evidence directory/);
expect(() => resolveSafeLiveEvidenceDirectory(path.join(repositoryDirectory, "test-results"))).toThrow(
/unsafe live evidence directory/,
);
});
test("refuses an unrelated directory merely named live-youtube without deleting its contents", () => {
const unrelatedParent = fs.mkdtempSync(path.join(os.tmpdir(), "unrelated-live-evidence-"));
const unrelatedEvidence = path.join(unrelatedParent, "live-youtube");
const protectedFile = path.join(unrelatedEvidence, "keep.txt");
try {
fs.mkdirSync(unrelatedEvidence, { recursive: true });
fs.writeFileSync(protectedFile, "keep");
expect(() =>
resetLiveAttemptEvidenceWorkspace({ attemptNumber: 1 }, { evidenceDirectory: unrelatedEvidence }),
).toThrow(/unsafe live evidence directory/);
expect(fs.readFileSync(protectedFile, "utf8")).toBe("keep");
} finally {
fs.rmSync(unrelatedParent, { force: true, recursive: true });
}
});
});
@@ -0,0 +1,9 @@
const DEFAULT_LIVE_SHORTS_NEXT_HOPS = 10;
const MIN_LIVE_SHORTS_NEXT_HOPS = 10;
const MAX_LIVE_SHORTS_NEXT_HOPS = 25;
module.exports = {
DEFAULT_LIVE_SHORTS_NEXT_HOPS,
MAX_LIVE_SHORTS_NEXT_HOPS,
MIN_LIVE_SHORTS_NEXT_HOPS,
};
+50 -19
View File
@@ -3,11 +3,21 @@ const fs = require("node:fs");
const path = require("node:path");
const extensionPackage = require("../../../package.json");
const userscriptVersion = require("../userscript-version.json");
const { assertCurrentLiveArtifact } = require("../e2e/live/live-artifact-readiness");
const { resolveCdpEndpoint } = require("./live-cdp-endpoint");
const {
DEFAULT_LIVE_SHORTS_NEXT_HOPS,
MAX_LIVE_SHORTS_NEXT_HOPS,
MIN_LIVE_SHORTS_NEXT_HOPS,
} = require("./live-navigation-constants");
const LIVE_VOTE_APPROVAL_WINDOW_SECONDS = 120;
const LIVE_VOTE_APPROVALS_DIRECTORY = path.resolve(__dirname, "../../../test-results/live-youtube-vote-approvals");
const DEFAULT_LIVE_NAV_CHANNEL_URL = "https://www.youtube.com/@SmashTrash";
const DEFAULT_LIVE_NAV_SHORT = "iKQhN7omLM4";
const DEFAULT_LIVE_CDP_CONNECT_TIMEOUT_MILLISECONDS = 120_000;
const MIN_LIVE_CDP_CONNECT_TIMEOUT_MILLISECONDS = 15_000;
const MAX_LIVE_CDP_CONNECT_TIMEOUT_MILLISECONDS = 300_000;
const DEFAULT_LIVE_NAV_CHANNEL_URL = "https://www.youtube.com/@MrBeast";
const DEFAULT_LIVE_NAV_SHORT = "5mU6SRS2Bxo";
const DEFAULT_LIVE_SIDEBAR_HOPS = 3;
const MAX_LIVE_SIDEBAR_HOPS = 10;
const VIDEO_ID_PATTERN = /^[A-Za-z0-9_-]{11}$/;
@@ -32,25 +42,16 @@ function requireVideoId(environment, name) {
return value;
}
function optionalVideoId(environment, name) {
const value = environment[name]?.trim();
if (!value) return null;
if (!VIDEO_ID_PATTERN.test(value)) {
throw new Error(`${name} must be an 11-character YouTube video ID when provided.`);
}
return value;
}
function optionalBoundedInteger(environment, name, defaultValue, maximum) {
function optionalBoundedInteger(environment, name, defaultValue, maximum, minimum = 1) {
const rawValue = environment[name]?.trim();
if (!rawValue) return defaultValue;
if (!/^\d+$/.test(rawValue)) {
throw new Error(`${name} must be a whole number from 1 to ${maximum}.`);
throw new Error(`${name} must be a whole number from ${minimum} to ${maximum}.`);
}
const value = Number(rawValue);
if (!Number.isSafeInteger(value) || value < 1 || value > maximum) {
throw new Error(`${name} must be a whole number from 1 to ${maximum}.`);
if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
throw new Error(`${name} must be a whole number from ${minimum} to ${maximum}.`);
}
return value;
}
@@ -149,7 +150,14 @@ function parseChannelUrl(value) {
return url.toString();
}
function readExpectedBuildId(runtime, { markerPaths = LIVE_BUILD_MARKER_PATHS, readFileSync = fs.readFileSync } = {}) {
function readExpectedBuildId(
runtime,
{
assertArtifactCurrent = assertCurrentLiveArtifact,
markerPaths = LIVE_BUILD_MARKER_PATHS,
readFileSync = fs.readFileSync,
} = {},
) {
const markerPath = markerPaths[runtime];
let marker;
try {
@@ -163,13 +171,14 @@ function readExpectedBuildId(runtime, { markerPaths = LIVE_BUILD_MARKER_PATHS, r
if (!LIVE_BUILD_ID_PATTERN.test(marker?.buildId)) {
throw new Error(`The ${runtime} live-build marker at ${markerPath} is malformed.`);
}
assertArtifactCurrent(runtime, marker.buildId);
return marker.buildId;
}
function readLiveOptions(
environment = process.env,
nowMilliseconds = Date.now(),
{ readBuildId = readExpectedBuildId } = {},
{ readBuildId = readExpectedBuildId, resolveEndpoint = resolveCdpEndpoint } = {},
) {
if (environment.RYD_LIVE_YOUTUBE !== "1") return null;
@@ -187,6 +196,9 @@ function readLiveOptions(
const watchA = requireVideoId(environment, "RYD_LIVE_WATCH_A");
const watchB = requireVideoId(environment, "RYD_LIVE_WATCH_B");
const short = requireVideoId(environment, "RYD_LIVE_SHORT");
const reactionShort = environment.RYD_LIVE_REACTION_SHORT?.trim()
? requireVideoId(environment, "RYD_LIVE_REACTION_SHORT")
: short;
if (watchA === watchB) throw new Error("RYD_LIVE_WATCH_A and RYD_LIVE_WATCH_B must be different videos.");
const expectedVersion =
@@ -194,7 +206,14 @@ function readLiveOptions(
(runtime === "userscript" ? userscriptVersion : extensionPackage.version);
return {
cdpEndpoint: environment.RYD_CDP_ENDPOINT?.trim() || "chrome",
cdpEndpoint: resolveEndpoint(environment.RYD_CDP_ENDPOINT?.trim() || "chrome", { environment }),
cdpConnectTimeoutMilliseconds: optionalBoundedInteger(
environment,
"RYD_CDP_CONNECT_TIMEOUT_MS",
DEFAULT_LIVE_CDP_CONNECT_TIMEOUT_MILLISECONDS,
MAX_LIVE_CDP_CONNECT_TIMEOUT_MILLISECONDS,
MIN_LIVE_CDP_CONNECT_TIMEOUT_MILLISECONDS,
),
expectedBuildId: readBuildId(runtime),
expectedChannel: requireChannelHandle(environment),
expectedVersion,
@@ -204,10 +223,18 @@ function readLiveOptions(
{ RYD_LIVE_NAV_SHORT: environment.RYD_LIVE_NAV_SHORT?.trim() || DEFAULT_LIVE_NAV_SHORT },
"RYD_LIVE_NAV_SHORT",
),
watch: optionalVideoId(environment, "RYD_LIVE_NAV_WATCH"),
shortsNextHops: optionalBoundedInteger(
environment,
"RYD_LIVE_SHORTS_NEXT_HOPS",
DEFAULT_LIVE_SHORTS_NEXT_HOPS,
MAX_LIVE_SHORTS_NEXT_HOPS,
MIN_LIVE_SHORTS_NEXT_HOPS,
),
watch: requireVideoId(environment, "RYD_LIVE_NAV_WATCH"),
},
playlistUrl: parsePlaylistUrl(requireValue(environment, "RYD_LIVE_PLAYLIST_URL"), watchA),
productionApiApproved: true,
reactionShort,
runtime,
sidebar: {
hopCount: optionalBoundedInteger(
@@ -227,8 +254,12 @@ module.exports = {
DEFAULT_LIVE_NAV_CHANNEL_URL,
DEFAULT_LIVE_NAV_SHORT,
DEFAULT_LIVE_SIDEBAR_HOPS,
DEFAULT_LIVE_SHORTS_NEXT_HOPS,
DEFAULT_LIVE_CDP_CONNECT_TIMEOUT_MILLISECONDS,
LIVE_VOTE_APPROVALS_DIRECTORY,
LIVE_VOTE_APPROVAL_WINDOW_SECONDS,
MAX_LIVE_SHORTS_NEXT_HOPS,
MIN_LIVE_SHORTS_NEXT_HOPS,
consumeLiveVoteApproval,
hasFreshVoteApproval,
liveVoteApproval,
+103 -3
View File
@@ -5,6 +5,8 @@ const {
DEFAULT_LIVE_NAV_CHANNEL_URL,
DEFAULT_LIVE_NAV_SHORT,
DEFAULT_LIVE_SIDEBAR_HOPS,
DEFAULT_LIVE_SHORTS_NEXT_HOPS,
DEFAULT_LIVE_CDP_CONNECT_TIMEOUT_MILLISECONDS,
LIVE_VOTE_APPROVALS_DIRECTORY,
LIVE_VOTE_APPROVAL_WINDOW_SECONDS,
consumeLiveVoteApproval,
@@ -24,6 +26,7 @@ const VALID_ENVIRONMENT = {
RYD_LIVE_WATCH_A: "abcdefghijk",
RYD_LIVE_WATCH_B: "zyxwvutsrqp",
RYD_LIVE_SHORT: "shortsabcde",
RYD_LIVE_NAV_WATCH: "navwatch001",
RYD_LIVE_PLAYLIST_URL: "https://www.youtube.com/watch?v=abcdefghijk&list=PL-test",
RYD_LIVE_EXPECTED_CHANNEL: "@ryd-test",
};
@@ -31,20 +34,24 @@ const VALID_ENVIRONMENT = {
function readLiveOptions(environment, nowMilliseconds = NOW) {
return readLiveOptionsFromEnvironment(environment, nowMilliseconds, {
readBuildId: () => EXPECTED_BUILD_ID,
resolveEndpoint: (value) => value,
});
}
describe("live YouTube options", () => {
test("reads the exact generated build ID from the selected runtime marker", () => {
const readFileSync = jest.fn(() => JSON.stringify({ buildId: EXPECTED_BUILD_ID }));
const assertArtifactCurrent = jest.fn();
expect(
readExpectedBuildId("userscript", {
assertArtifactCurrent,
markerPaths: { userscript: "owned-live-build.json" },
readFileSync,
}),
).toBe(EXPECTED_BUILD_ID);
expect(readFileSync).toHaveBeenCalledWith("owned-live-build.json", "utf8");
expect(assertArtifactCurrent).toHaveBeenCalledWith("userscript", EXPECTED_BUILD_ID);
});
test.each([
@@ -60,12 +67,29 @@ describe("live YouTube options", () => {
])("rejects a %s generated live-build marker", (_label, readFileSync, expectedMessage) => {
expect(() =>
readExpectedBuildId("extension", {
assertArtifactCurrent: jest.fn(),
markerPaths: { extension: "owned-live-build.json" },
readFileSync,
}),
).toThrow(expectedMessage);
});
test("rejects a live marker when the corresponding artifact receipt is stale", () => {
const staleArtifact = new Error("Userscript build inputs changed after the artifact was generated.");
const assertArtifactCurrent = jest.fn(() => {
throw staleArtifact;
});
expect(() =>
readExpectedBuildId("userscript", {
assertArtifactCurrent,
markerPaths: { userscript: "owned-live-build.json" },
readFileSync: () => JSON.stringify({ buildId: EXPECTED_BUILD_ID }),
}),
).toThrow(staleArtifact);
expect(assertArtifactCurrent).toHaveBeenCalledWith("userscript", EXPECTED_BUILD_ID);
});
test("stores consumed vote approvals outside Playwright's cleaned output directory", () => {
const playwrightOutputDirectory = path.resolve(__dirname, "../../../test-results/live-youtube");
expect(LIVE_VOTE_APPROVALS_DIRECTORY).not.toBe(playwrightOutputDirectory);
@@ -76,6 +100,11 @@ describe("live YouTube options", () => {
expect(readLiveOptions({})).toBeNull();
});
test("defaults to the currently verified high-volume channel navigation fixture", () => {
expect(DEFAULT_LIVE_NAV_CHANNEL_URL).toBe("https://www.youtube.com/@MrBeast");
expect(DEFAULT_LIVE_NAV_SHORT).toBe("5mU6SRS2Bxo");
});
test("requires production API acknowledgement", () => {
expect(() => readLiveOptions({ ...VALID_ENVIRONMENT, RYD_LIVE_PRODUCTION_API: undefined })).toThrow(
"RYD_LIVE_PRODUCTION_API=1",
@@ -116,12 +145,15 @@ describe("live YouTube options", () => {
test.each(["userscript", "extension"])("accepts the %s runtime", (runtime) => {
expect(readLiveOptions({ ...VALID_ENVIRONMENT, RYD_LIVE_RUNTIME: runtime })).toMatchObject({
cdpEndpoint: "chrome",
cdpConnectTimeoutMilliseconds: DEFAULT_LIVE_CDP_CONNECT_TIMEOUT_MILLISECONDS,
expectedBuildId: EXPECTED_BUILD_ID,
navigation: {
channelUrl: DEFAULT_LIVE_NAV_CHANNEL_URL,
short: DEFAULT_LIVE_NAV_SHORT,
watch: null,
shortsNextHops: DEFAULT_LIVE_SHORTS_NEXT_HOPS,
watch: "navwatch001",
},
reactionShort: "shortsabcde",
runtime,
sidebar: { hopCount: DEFAULT_LIVE_SIDEBAR_HOPS },
short: "shortsabcde",
@@ -135,19 +167,68 @@ describe("live YouTube options", () => {
const result = readLiveOptionsFromEnvironment(
{ ...VALID_ENVIRONMENT, RYD_LIVE_EXPECTED_BUILD_ID: "f".repeat(32) },
NOW,
{ readBuildId },
{ readBuildId, resolveEndpoint: (value) => value },
);
expect(readBuildId).toHaveBeenCalledWith("userscript");
expect(result.expectedBuildId).toBe(EXPECTED_BUILD_ID);
});
test("resolves the configured browser endpoint with the same environment used for live options", () => {
const resolveEndpoint = jest.fn(() => "ws://127.0.0.1:60011/devtools/browser/session");
const environment = { ...VALID_ENVIRONMENT, RYD_CDP_ENDPOINT: " 127.0.0.1:60011 " };
const result = readLiveOptionsFromEnvironment(environment, NOW, {
readBuildId: () => EXPECTED_BUILD_ID,
resolveEndpoint,
});
expect(resolveEndpoint).toHaveBeenCalledWith("127.0.0.1:60011", { environment });
expect(result.cdpEndpoint).toBe("ws://127.0.0.1:60011/devtools/browser/session");
});
test("accepts a bounded remote-debugging approval timeout", () => {
expect(readLiveOptions({ ...VALID_ENVIRONMENT, RYD_CDP_CONNECT_TIMEOUT_MS: "180000" })).toMatchObject({
cdpConnectTimeoutMilliseconds: 180_000,
});
});
test.each(["0", "14999", "300001", "1.5", "two minutes", "-1"])(
"rejects invalid remote-debugging approval timeout %p",
(timeout) => {
expect(() => readLiveOptions({ ...VALID_ENVIRONMENT, RYD_CDP_CONNECT_TIMEOUT_MS: timeout })).toThrow(
/RYD_CDP_CONNECT_TIMEOUT_MS.*whole number from 15000 to 300000/,
);
},
);
test("accepts a bounded sidebar stress hop count", () => {
expect(readLiveOptions({ ...VALID_ENVIRONMENT, RYD_LIVE_SIDEBAR_HOPS: "5" })).toMatchObject({
sidebar: { hopCount: 5 },
});
});
test("defaults the live Shorts stress to ten successful Next samples", () => {
expect(readLiveOptions(VALID_ENVIRONMENT)).toMatchObject({
navigation: { shortsNextHops: 10 },
});
});
test("accepts a larger bounded successful live Shorts Next count", () => {
expect(readLiveOptions({ ...VALID_ENVIRONMENT, RYD_LIVE_SHORTS_NEXT_HOPS: "15" })).toMatchObject({
navigation: { shortsNextHops: 15 },
});
});
test.each(["0", "1", "9", "26", "10.5", "three", "-1"])(
"rejects live Shorts Next count below ten or outside the bound: %p",
(hopCount) => {
expect(() => readLiveOptions({ ...VALID_ENVIRONMENT, RYD_LIVE_SHORTS_NEXT_HOPS: hopCount })).toThrow(
/RYD_LIVE_SHORTS_NEXT_HOPS.*whole number from 10 to 25/,
);
},
);
test.each(["0", "11", "1.5", "three", "-1"])("rejects invalid sidebar stress hop count %p", (hopCount) => {
expect(() => readLiveOptions({ ...VALID_ENVIRONMENT, RYD_LIVE_SIDEBAR_HOPS: hopCount })).toThrow(
/RYD_LIVE_SIDEBAR_HOPS.*whole number from 1 to 10/,
@@ -171,6 +252,19 @@ describe("live YouTube options", () => {
});
});
test("accepts a dedicated reaction Short without changing the read-only Short", () => {
expect(readLiveOptions({ ...VALID_ENVIRONMENT, RYD_LIVE_REACTION_SHORT: "reactshort1" })).toMatchObject({
reactionShort: "reactshort1",
short: "shortsabcde",
});
});
test.each(["invalid", "abcdefghij!", "abcdefghijkl"])("rejects malformed reaction Short ID %p", (videoId) => {
expect(() => readLiveOptions({ ...VALID_ENVIRONMENT, RYD_LIVE_REACTION_SHORT: videoId })).toThrow(
"RYD_LIVE_REACTION_SHORT must be an 11-character YouTube video ID",
);
});
test.each([
"http://www.youtube.com/@SmashTrash",
"https://example.com/@SmashTrash",
@@ -184,7 +278,13 @@ describe("live YouTube options", () => {
);
});
test.each(["RYD_LIVE_NAV_SHORT", "RYD_LIVE_NAV_WATCH"])("validates optional navigation ID %s", (name) => {
test("requires an exact channel Watch target", () => {
expect(() => readLiveOptions({ ...VALID_ENVIRONMENT, RYD_LIVE_NAV_WATCH: undefined })).toThrow(
"RYD_LIVE_NAV_WATCH is required",
);
});
test.each(["RYD_LIVE_NAV_SHORT", "RYD_LIVE_NAV_WATCH"])("validates navigation ID %s", (name) => {
expect(() => readLiveOptions({ ...VALID_ENVIRONMENT, [name]: "invalid" })).toThrow("11-character YouTube video ID");
});
@@ -6,6 +6,7 @@ class FakeBrowserContext {
this.routes = [];
this.routeCalls = 0;
this.unrouteCalls = 0;
this.unrouteImplementation = null;
}
on(event, handler) {
@@ -25,6 +26,7 @@ class FakeBrowserContext {
async unroute(matcher, handler) {
this.unrouteCalls += 1;
this.routes = this.routes.filter((route) => route.matcher !== matcher || route.handler !== handler);
if (this.unrouteImplementation) await this.unrouteImplementation();
}
async dispatch(request) {
@@ -45,12 +47,12 @@ class FakeBrowserContext {
}
}
function createDriver(context) {
function createDriver(context, options = {}) {
const page = {
setDefaultNavigationTimeout: jest.fn(),
setDefaultTimeout: jest.fn(),
};
return new LiveYoutubeDriver(page, context);
return new LiveYoutubeDriver(page, context, options);
}
function request(method, url) {
@@ -116,4 +118,59 @@ describe("live read-only production-interaction guard", () => {
expect(context.unrouteCalls).toBe(1);
expect(context.routes).toEqual([]);
});
test("bounds a stalled route removal, clears the guard, and allows a fresh guard", async () => {
const context = new FakeBrowserContext();
const reportProgress = jest.fn();
const driver = createDriver(context, {
readOnlyInteractionGuardCleanupTimeout: 10,
reportProgress,
});
context.unrouteImplementation = () => new Promise(() => {});
await expect(driver.withNoProductionInteractions(async () => "first action complete")).rejects.toThrow(
"Timed out after 10ms while removing the live read-only production-interaction route.",
);
expect(driver.readOnlyInteractionGuard).toBeNull();
expect(context.listeners.get("request")?.size ?? 0).toBe(0);
expect(context.routes).toEqual([]);
expect(reportProgress).toHaveBeenCalledWith("read-only-interaction-guard.unroute-started", { timeoutMs: 10 });
expect(reportProgress).toHaveBeenCalledWith("read-only-interaction-guard.unroute-failed", {
message: "Timed out after 10ms while removing the live read-only production-interaction route.",
timeoutMs: 10,
});
context.unrouteImplementation = null;
await expect(driver.withNoProductionInteractions(async () => "second action complete")).resolves.toBe(
"second action complete",
);
expect(context.routeCalls).toBe(2);
expect(context.unrouteCalls).toBe(2);
});
test("preserves both the action failure and a stalled route-removal failure", async () => {
const context = new FakeBrowserContext();
const driver = createDriver(context, { readOnlyInteractionGuardCleanupTimeout: 10 });
const actionError = new Error("read-only action failed");
context.unrouteImplementation = () => new Promise(() => {});
const failure = await driver
.withNoProductionInteractions(async () => {
throw actionError;
})
.catch((error) => error);
expect(failure).toBeInstanceOf(AggregateError);
expect(failure.message).toBe("The read-only live scenario and its production-interaction guard both failed.");
expect(failure.errors).toHaveLength(2);
expect(failure.errors[0]).toBe(actionError);
expect(failure.errors[1]).toEqual(
expect.objectContaining({
message: "Timed out after 10ms while removing the live read-only production-interaction route.",
}),
);
expect(driver.readOnlyInteractionGuard).toBeNull();
expect(context.listeners.get("request")?.size ?? 0).toBe(0);
});
});
@@ -0,0 +1,120 @@
const { AttributedRuntimeTrafficLedger, attributeRuntimeRequest } = require("../e2e/live/live-youtube-driver");
const SELECTED_EXTENSION_ID = "abcdefghijklmnopabcdefghijklmnop";
const EXPECTED_BUILD_ID = "0123456789abcdef0123456789abcdef";
function createEventContext(pages = []) {
const listeners = new Map();
return {
context: {
off: jest.fn((event, listener) => listeners.get(event)?.delete(listener)),
on: jest.fn((event, listener) => {
if (!listeners.has(event)) listeners.set(event, new Set());
listeners.get(event).add(listener);
}),
pages: jest.fn(() => pages),
},
emit(event, value) {
listeners.get(event)?.forEach((listener) => listener(value));
},
listeners,
};
}
function pageRequest(page, { method, pathname, videoId }) {
return {
frame: () => ({ page: () => page }),
method: () => method,
postDataJSON: () => ({ userId: "A".repeat(36), value: -1, videoId }),
url: () => `https://returnyoutubedislikeapi.com${pathname}${pathname === "/votes" ? `?videoId=${videoId}` : ""}`,
};
}
async function advanceFakeTime(milliseconds) {
for (let elapsed = 0; elapsed < milliseconds; elapsed += 25) {
jest.advanceTimersByTime(Math.min(25, milliseconds - elapsed));
await Promise.resolve();
await Promise.resolve();
}
}
describe("run-wide attributed runtime traffic ledger", () => {
afterEach(() => {
jest.useRealTimers();
});
test.each([
["late vote-data GET", { method: "GET", pathname: "/votes" }],
["late interaction POST", { method: "POST", pathname: "/interact/vote" }],
])("rejects a %s emitted after the stage listener detached", async (_label, requestOptions) => {
jest.useFakeTimers({ doNotFake: ["performance"] });
const page = {};
const harness = createEventContext([page]);
const ledger = new AttributedRuntimeTrafficLedger(harness.context, page, (request) =>
attributeRuntimeRequest(request, page, "userscript"),
);
const detachedStageListener = jest.fn();
harness.context.on("request", detachedStageListener);
harness.context.off("request", detachedStageListener);
const request = pageRequest(page, { ...requestOptions, videoId: "abcdefghijk" });
const audit = ledger.assertNoUnclaimed({ label: "detached stage", quietMs: 100, timeoutMs: 500 });
const rejection = expect(audit).rejects.toThrow(/escaped every active audit/);
setTimeout(() => harness.emit("request", request), 50);
await advanceFakeTime(250);
await rejection;
expect(detachedStageListener).not.toHaveBeenCalled();
expect(ledger.records).toEqual([
expect.objectContaining({
method: requestOptions.method,
pathname: requestOptions.pathname,
videoId: "abcdefghijk",
}),
]);
ledger.stop();
});
test("accepts delayed traffic only when an active stage claims that exact request", async () => {
jest.useFakeTimers({ doNotFake: ["performance"] });
const page = {};
const harness = createEventContext([page]);
const ledger = new AttributedRuntimeTrafficLedger(harness.context, page, (request) =>
attributeRuntimeRequest(request, page, "userscript"),
);
const request = pageRequest(page, { method: "GET", pathname: "/votes", videoId: "abcdefghijk" });
harness.context.on("request", (candidate) => ledger.claim(candidate, "exact-votes-stage"));
const audit = ledger.assertNoUnclaimed({ label: "claimed stage", quietMs: 100, timeoutMs: 500 });
setTimeout(() => harness.emit("request", request), 50);
await advanceFakeTime(250);
await expect(audit).resolves.toEqual({ attributedRequests: 1, label: "claimed stage" });
ledger.stop();
});
test("retains same-video other-tab ambiguity from request time after that tab navigates away", async () => {
const page = { url: () => "https://www.youtube.com/watch?v=abcdefghijk" };
let otherUrl = "https://www.youtube.com/shorts/abcdefghijk";
const otherPage = { url: () => otherUrl };
const harness = createEventContext([page, otherPage]);
const ledger = new AttributedRuntimeTrafficLedger(harness.context, page, (request) =>
attributeRuntimeRequest(request, page, "extension", SELECTED_EXTENSION_ID, EXPECTED_BUILD_ID),
);
const request = {
frame: () => {
throw new Error("Service Worker requests do not have a frame");
},
method: () => "GET",
serviceWorker: () => ({ url: () => `chrome-extension://${SELECTED_EXTENSION_ID}/ryd.background.js` }),
url: () => "https://returnyoutubedislikeapi.com/votes?videoId=abcdefghijk",
};
harness.emit("request", request);
expect(ledger.claim(request, "exact-votes-stage")).toBe(true);
otherUrl = "https://www.youtube.com/watch?v=lmnopqrstuv";
await expect(ledger.assertNoUnclaimed({ quietMs: 0 })).rejects.toThrow(/same video open at send time/);
ledger.stop();
});
});
File diff suppressed because it is too large Load Diff
@@ -1,7 +1,8 @@
const { assertLogicalVoteHandshake } = require("../e2e/live/live-youtube-driver");
const VIDEO_ID = "abcdefghijk";
const USER_ID = "shared-user-id";
const USER_ID = "A".repeat(36);
const SOLUTION = Buffer.alloc(4).toString("base64");
function vote({ status = 200, userId = USER_ID, value = 1, videoId = VIDEO_ID } = {}) {
return {
@@ -14,7 +15,7 @@ function vote({ status = 200, userId = USER_ID, value = 1, videoId = VIDEO_ID }
function confirmation({ confirmed = true, status = 200, userId = USER_ID, videoId = VIDEO_ID } = {}) {
return {
body: { solution: "proof", userId, videoId },
body: { solution: SOLUTION, userId, videoId },
pathname: "/interact/confirmVote",
responseBody: confirmed,
responseError: null,
@@ -42,13 +43,24 @@ describe("live logical vote handshake validation", () => {
});
test.each([
["user", [vote(), vote({ userId: "another-user" }), confirmation()], /different user IDs/],
["user", [vote(), vote({ userId: "B".repeat(36) }), confirmation()], /different user IDs/],
["video", [vote(), vote({ videoId: "lmnopqrstuv" }), confirmation()], /different video/],
["value", [vote(), vote({ value: 0 }), confirmation()], /changed the requested vote value/],
])("rejects a retry with a mismatched %s", (_field, records, message) => {
expect(() => assertLogicalVoteHandshake(records, VIDEO_ID, 1)).toThrow(message);
});
test("rejects a complete handshake that targets the stale origin video", () => {
const staleVideoId = "lmnopqrstuv";
expect(() =>
assertLogicalVoteHandshake(
[vote({ videoId: staleVideoId }), confirmation({ videoId: staleVideoId })],
VIDEO_ID,
1,
),
).toThrow(/targeted a different video/);
});
test.each([
["extra confirmation", [vote(), confirmation(), confirmation()]],
["other interaction", [vote(), { ...vote(), pathname: "/interact/other" }, confirmation()]],
@@ -65,6 +77,27 @@ describe("live logical vote handshake validation", () => {
);
});
test.each([
["short user ID", [vote({ userId: "short" }), confirmation({ userId: "short" })], /36-character/],
[
"extra vote body key",
[{ ...vote(), body: { ...vote().body, extra: true } }, confirmation()],
/exactly userId, value, and videoId/,
],
[
"extra confirmation body key",
[vote(), { ...confirmation(), body: { ...confirmation().body, extra: true } }],
/exactly solution, userId, and videoId/,
],
[
"non-four-byte proof",
[vote(), { ...confirmation(), body: { ...confirmation().body, solution: "AA==" } }],
/four-byte proof/,
],
])("rejects a handshake with a %s", (_label, records, message) => {
expect(() => assertLogicalVoteHandshake(records, VIDEO_ID, 1)).toThrow(message);
});
test("rejects a failed vote or confirmation response", () => {
expect(() => assertLogicalVoteHandshake([vote({ status: 500 }), confirmation()], VIDEO_ID, 1)).toThrow(
/Vote request failed with HTTP 500/,
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,132 @@
const {
assertWatchRatioData,
colorContrastRatio,
formattedWatchTooltipCandidates,
normalizeTooltipText,
parseCssColor,
} = require("../e2e/live/watch-ratio-audit");
const VIDEO_ID = "abcdefghijk";
const COUNTS = Object.freeze({ dislikes: 250, likes: 750 });
function measurement(overrides = {}) {
return {
appearance: {
inlineFillWidth: "75%",
negativeTrackColor: "rgb(126, 126, 126)",
pageBackgroundColor: "rgb(15, 15, 15)",
tooltipText: "750 / 250",
wrapperVideoId: VIDEO_ID,
...overrides.appearance,
},
geometry: {
bar: { width: 150 },
container: { width: 200 },
...overrides.geometry,
},
};
}
function assertMeasurement(value, options = {}) {
return assertWatchRatioData(value, {
expectedCounts: COUNTS,
expectedTooltipCandidates: ["750 / 250"],
runtime: "extension",
videoId: VIDEO_ID,
...options,
});
}
describe("live Watch ratio data audit", () => {
test.each([
["dark", "rgb(126, 126, 126)", "rgb(15, 15, 15)"],
["light", "rgb(115, 115, 115)", "rgb(255, 255, 255)"],
])("accepts an owned exact-ratio bar with a visible negative track in the %s theme", (_theme, track, page) => {
expect(
assertMeasurement(measurement({ appearance: { negativeTrackColor: track, pageBackgroundColor: page } })),
).toMatchObject({
expectedRatio: 0.75,
inlineRatio: 0.75,
renderedRatio: 0.75,
tooltipText: "750 / 250",
wrapperVideoId: VIDEO_ID,
});
});
test.each([null, "zyxwvutsrqp"])("rejects a Watch ratio wrapper owned by %s", (wrapperVideoId) => {
expect(() => assertMeasurement(measurement({ appearance: { wrapperVideoId } }))).toThrow(
/Watch ratio bar is owned by/,
);
});
test.each([
["inline width", { appearance: { inlineFillWidth: "25%" } }],
["rendered geometry", { geometry: { bar: { width: 50 } } }],
])("rejects a wrong %s even when the displayed count is unchanged", (_label, override) => {
expect(() => assertMeasurement(measurement(override))).toThrow(
/expected 75\.000% from exact API counts|expected 75\.00%/,
);
});
test("rejects a stale tooltip even when ownership, count text, and ratio are valid", () => {
expect(() => assertMeasurement(measurement({ appearance: { tooltipText: "900 / 100" } }))).toThrow(
/tooltip.*does not exactly match the API counts/,
);
});
test("accepts the exact configured percentage variants", () => {
expect(
assertMeasurement(measurement({ appearance: { tooltipText: "750 / 250 - 75%" } }), {
expectedTooltipCandidates: ["750 / 250", "750 / 250 - 75%", "750 / 250 - 25%"],
}).tooltipText,
).toBe("750 / 250 - 75%");
});
test("derives every configured tooltip form from the exact API counts and browser locale", () => {
expect(formattedWatchTooltipCandidates(COUNTS, "en-US")).toEqual([
{ form: "counts", text: "750 / 250" },
{ form: "counts-with-like-percentage", text: "750 / 250 - 75%" },
{ form: "counts-with-dislike-percentage", text: "750 / 250 - 25%" },
{ form: "both-percentages", text: "75% / 25%" },
{ form: "like-percentage", text: "75%" },
{ form: "dislike-percentage", text: "25%" },
]);
});
test.each([
["transparent", "rgba(115, 115, 115, 0)"],
["ancestor opacity", "rgb(115, 115, 115)", "rgb(15, 15, 15)", 0.01],
["near-background dark", "rgb(18, 18, 18)"],
["near-background light", "rgb(248, 248, 248)", "rgb(255, 255, 255)"],
])("rejects an effectively invisible negative track: %s", (_label, track, page = "rgb(15, 15, 15)", opacity = 1) => {
expect(() =>
assertMeasurement(
measurement({
appearance: { negativeTrackColor: track, negativeTrackOpacity: opacity, pageBackgroundColor: page },
}),
),
).toThrow(/negative track is effectively invisible/);
});
test("rejects a negative-track background image instead of guessing its contrast", () => {
expect(() =>
assertMeasurement(measurement({ appearance: { negativeTrackBackgroundImage: "linear-gradient(#000, #fff)" } })),
).toThrow(/background image whose effective contrast cannot be audited/);
});
test("normalizes non-breaking spaces and directional formatting in tooltip text", () => {
expect(normalizeTooltipText("\u2066 750\u00a0/\u00a0250 \u2069")).toBe("750 / 250");
});
test.each([
["#737373", { a: 1, b: 115, g: 115, r: 115 }],
["rgba(115, 115, 115, 50%)", { a: 0.5, b: 115, g: 115, r: 115 }],
["color(srgb 0.5 0.25 0 / 0.75)", { a: 0.75, b: 0, g: 63.75, r: 127.5 }],
])("parses computed CSS color %s", (value, expected) => {
expect(parseCssColor(value)).toEqual(expected);
});
test("computes the standard black-to-white contrast ratio", () => {
expect(colorContrastRatio(parseCssColor("#000"), parseCssColor("#fff"))).toBeCloseTo(21, 5);
});
});