mirror of
https://github.com/Alvin-Zilverstand/return-youtube-dislike.git
synced 2026-09-12 09:58:20 +02:00
e500088e65
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.
453 lines
18 KiB
JavaScript
453 lines
18 KiB
JavaScript
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 });
|
|
}
|
|
});
|
|
});
|