mirror of
https://github.com/Alvin-Zilverstand/return-youtube-dislike.git
synced 2026-10-28 07:40:27 +01:00
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 });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|