Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions apps/extension/src/tools/__tests__/download-trigger-intent.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { describe, expect, it, vi } from "vitest";
import { resolveDownloadTriggerUrl } from "../download-trigger-intent";
import type { ResolvedActionTarget } from "../interaction";
import type { CdpRunner } from "../shared";

const target: ResolvedActionTarget = {
tab: { tabId: 4, windowId: 100, active: true },
backendNodeId: 123,
cdpTarget: { tabId: 4 },
};

describe("download trigger intent", () => {
it("returns a validated HTTP URL from a target-blank anchor", async () => {
const send = vi.fn(async (_tabId: number, method: string) => {
if (method === "DOM.resolveNode") return { object: { objectId: "anchor" } };
if (method === "Runtime.callFunctionOn") {
return { result: { value: "https://example.test/report.csv" } };
}
return {};
}) as unknown as CdpRunner["send"];

await expect(resolveDownloadTriggerUrl({ send }, target)).resolves.toBe(
"https://example.test/report.csv",
);
expect(send).toHaveBeenCalledWith(4, "Runtime.releaseObject", { objectId: "anchor" });
});

it("rejects non-HTTP results", async () => {
const send = vi.fn(async (_tabId: number, method: string) => {
if (method === "DOM.resolveNode") return { object: { objectId: "anchor" } };
if (method === "Runtime.callFunctionOn") {
return { result: { value: "javascript:download()" } };
}
return {};
}) as unknown as CdpRunner["send"];

await expect(resolveDownloadTriggerUrl({ send }, target)).resolves.toBeUndefined();
});
});
220 changes: 214 additions & 6 deletions apps/extension/src/tools/__tests__/file-transfer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -626,7 +626,11 @@ describe("file transfer tools", () => {
const result = await handleDownload(
manager,
{ session_id: "s1", ref: "@e3", browser_relative_dir: "BrowserSkill/tr_1" },
{ cdp, tabsApi: tabsApi(), downloads },
{
cdp,
tabsApi: tabsApi(),
downloads,
},
);

expect(suggested).toEqual({
Expand Down Expand Up @@ -682,14 +686,17 @@ describe("file transfer tools", () => {
downloads,
browserRelativeDir: "BrowserSkill/tr_1",
timeoutMs: 5,
trigger: async () => {
trigger: async (observer) => {
const armError = observer.beforePressDispatch();
if (armError) return armError;
cdpEvent?.({ tabId: 4, sessionId: "other-child" }, "Page.downloadWillBegin", {
url: unrelated.url,
suggestedFilename: unrelated.filename,
});
onDeterminingFilename.emit(unrelated, (suggestion) => {
defaultSuggestionCalled = suggestion === undefined;
});
observer.afterPressDispatch();
return { tab_id: 4, x: 10, y: 10 };
},
});
Expand All @@ -702,6 +709,195 @@ describe("file transfer tools", () => {
});
});

it("captures a unique download matching a trigger-authorized URL", async () => {
const onCreated = fakeEvent<(item: chrome.downloads.DownloadItem) => void>();
const onChanged = fakeEvent<(delta: chrome.downloads.DownloadDelta) => void>();
const onDeterminingFilename =
fakeEvent<
(
item: chrome.downloads.DownloadItem,
suggest: (suggestion?: chrome.downloads.DownloadFilenameSuggestion) => void,
) => void | true
>();
const initial = {
id: 18,
url: "https://example.test/popup.bin",
finalUrl: "https://example.test/popup.bin",
filename: "popup.bin",
state: "in_progress",
fileSize: -1,
totalBytes: 4,
mime: "application/octet-stream",
danger: "safe",
} as chrome.downloads.DownloadItem;
const complete = {
...initial,
filename: "/profile/Downloads/BrowserSkill/tr_2/popup.bin",
state: "complete",
fileSize: 4,
} as chrome.downloads.DownloadItem;
const downloads: DownloadsApi = {
onCreated,
onChanged,
onDeterminingFilename,
search: vi.fn(async () => [complete]),
cancel: vi.fn(async () => {}),
removeFile: vi.fn(async () => {}),
};
const cdp: CdpRunner = {
send: vi.fn(async () => ({})) as unknown as CdpRunner["send"],
onEvent: () => ({ dispose: vi.fn() }),
};

const result = await captureBrowserDownload({
cdp,
target: { tabId: 4 },
expectedUrl: initial.url,
downloads,
browserRelativeDir: "BrowserSkill/tr_2",
timeoutMs: 200,
trigger: async (observer) => {
const armError = observer.beforePressDispatch();
if (armError) return armError;
await new Promise<void>((resolve) => {
onDeterminingFilename.emit(initial, () => resolve());
});
onCreated.emit(complete);
observer.afterPressDispatch();
return { tab_id: 4, x: 10, y: 10 };
},
});

expect(result).toMatchObject({
item: { id: 18, filename: complete.filename, state: "complete" },
});
});

it("does not expose URL attribution before mouse press dispatch", async () => {
const onCreated = fakeEvent<(item: chrome.downloads.DownloadItem) => void>();
const onChanged = fakeEvent<(delta: chrome.downloads.DownloadDelta) => void>();
const onDeterminingFilename =
fakeEvent<
(
item: chrome.downloads.DownloadItem,
suggest: (suggestion?: chrome.downloads.DownloadFilenameSuggestion) => void,
) => void | true
>();
const candidate = (id: number) =>
({
id,
url: "https://example.test/popup.bin",
finalUrl: "https://example.test/popup.bin",
filename: "popup.bin",
state: "in_progress",
fileSize: -1,
totalBytes: 4,
}) as chrome.downloads.DownloadItem;
const completed = {
...candidate(42),
filename: "/profile/Downloads/BrowserSkill/tr_42/popup.bin",
state: "complete",
fileSize: 4,
} as chrome.downloads.DownloadItem;
const downloads: DownloadsApi = {
onCreated,
onChanged,
onDeterminingFilename,
search: vi.fn(async () => [completed]),
cancel: vi.fn(async () => {}),
removeFile: vi.fn(async () => {}),
};
const cdp: CdpRunner = {
send: vi.fn(async () => ({})) as CdpRunner["send"],
onEvent: () => ({ dispose: vi.fn() }),
};
let prePressSuggestionCalled = false;

const result = await captureBrowserDownload({
cdp,
target: { tabId: 4 },
expectedUrl: completed.url,
downloads,
browserRelativeDir: "BrowserSkill/tr_42",
timeoutMs: 1_000,
trigger: async (observer) => {
onDeterminingFilename.emit(candidate(41), () => {
prePressSuggestionCalled = true;
});
const armError = observer.beforePressDispatch();
if (armError) return armError;
onDeterminingFilename.emit(candidate(42), () => {});
onCreated.emit(completed);
observer.afterPressDispatch();
return { tab_id: 4, x: 10, y: 10 };
},
});

expect(prePressSuggestionCalled).toBe(false);
expect(result).toMatchObject({ item: { id: 42, state: "complete" } });
expect(downloads.cancel).not.toHaveBeenCalled();
});

it("finishes a URL-attributed download when mouse release fails", async () => {
const onCreated = fakeEvent<(item: chrome.downloads.DownloadItem) => void>();
const onChanged = fakeEvent<(delta: chrome.downloads.DownloadDelta) => void>();
const onDeterminingFilename =
fakeEvent<
(
item: chrome.downloads.DownloadItem,
suggest: (suggestion?: chrome.downloads.DownloadFilenameSuggestion) => void,
) => void | true
>();
const initial = {
id: 43,
url: "https://example.test/release-race.bin",
finalUrl: "https://example.test/release-race.bin",
filename: "release-race.bin",
state: "in_progress",
fileSize: -1,
totalBytes: 4,
} as chrome.downloads.DownloadItem;
const completed = {
...initial,
filename: "/profile/Downloads/BrowserSkill/tr_43/release-race.bin",
state: "complete",
fileSize: 4,
} as chrome.downloads.DownloadItem;
const downloads: DownloadsApi = {
onCreated,
onChanged,
onDeterminingFilename,
search: vi.fn(async () => [completed]),
cancel: vi.fn(async () => {}),
removeFile: vi.fn(async () => {}),
};
const cdp: CdpRunner = {
send: vi.fn(async () => ({})) as CdpRunner["send"],
onEvent: () => ({ dispose: vi.fn() }),
};

const result = await captureBrowserDownload({
cdp,
target: { tabId: 4 },
expectedUrl: initial.url,
downloads,
browserRelativeDir: "BrowserSkill/tr_43",
timeoutMs: 1_000,
trigger: async (observer) => {
const armError = observer.beforePressDispatch();
if (armError) return armError;
onDeterminingFilename.emit(initial, () => {});
onCreated.emit(completed);
observer.afterPressDispatch();
return { code: "cdp_failed", message: "mouseReleased failed" };
},
});

expect(result).toMatchObject({ item: { id: 43, state: "complete" } });
expect(downloads.cancel).not.toHaveBeenCalled();
expect(downloads.removeFile).not.toHaveBeenCalled();
});

it("correlates a filename candidate that arrives before the CDP intent", async () => {
const onCreated = fakeEvent<(item: chrome.downloads.DownloadItem) => void>();
const onChanged = fakeEvent<(delta: chrome.downloads.DownloadDelta) => void>();
Expand Down Expand Up @@ -751,7 +947,9 @@ describe("file transfer tools", () => {
downloads,
browserRelativeDir: "BrowserSkill/tr_21",
timeoutMs: 1_000,
trigger: async () => {
trigger: async (observer) => {
const armError = observer.beforePressDispatch();
if (armError) return armError;
const suggested = new Promise<void>((resolve) => {
onDeterminingFilename.emit(initial, (value) => {
suggestion = value;
Expand All @@ -764,6 +962,7 @@ describe("file transfer tools", () => {
});
await suggested;
onCreated.emit(complete);
observer.afterPressDispatch();
return { tab_id: 4, x: 10, y: 10 };
},
});
Expand Down Expand Up @@ -827,7 +1026,9 @@ describe("file transfer tools", () => {
browserRelativeDir: "BrowserSkill/tr_22",
maxByteSize: 4,
timeoutMs: 1_000,
trigger: async () => {
trigger: async (observer) => {
const armError = observer.beforePressDispatch();
if (armError) return armError;
cdpEvent?.({ tabId: 4 }, "Page.downloadWillBegin", {
url: initial.url,
suggestedFilename: initial.filename,
Expand All @@ -837,6 +1038,7 @@ describe("file transfer tools", () => {
});
onCreated.emit(initial);
onChanged.emit({ id: initial.id, state: { current: "complete" } });
observer.afterPressDispatch();
return { tab_id: 4, x: 10, y: 10 };
},
});
Expand Down Expand Up @@ -902,7 +1104,9 @@ describe("file transfer tools", () => {
downloads,
browserRelativeDir: "BrowserSkill/tr_23",
timeoutMs: 80,
trigger: async () => {
trigger: async (observer) => {
const armError = observer.beforePressDispatch();
if (armError) return armError;
cdpEvent?.({ tabId: 4 }, "Page.downloadWillBegin", {
url: initial.url,
suggestedFilename: initial.filename,
Expand All @@ -911,6 +1115,7 @@ describe("file transfer tools", () => {
onDeterminingFilename.emit(initial, () => resolve());
});
onCreated.emit(initial);
observer.afterPressDispatch();
return { tab_id: 4, x: 10, y: 10 };
},
});
Expand Down Expand Up @@ -966,7 +1171,9 @@ describe("file transfer tools", () => {
downloads,
browserRelativeDir: "BrowserSkill/tr_ambiguous",
timeoutMs: 100,
trigger: async () => {
trigger: async (observer) => {
const armError = observer.beforePressDispatch();
if (armError) return armError;
cdpEvent?.({ tabId: 4 }, "Page.downloadWillBegin", {
url: "https://example.test/same.bin",
suggestedFilename: "same.bin",
Expand All @@ -976,6 +1183,7 @@ describe("file transfer tools", () => {
if (value === undefined) defaults.push(id);
});
}
observer.afterPressDispatch();
return { tab_id: 4, x: 10, y: 10 };
},
});
Expand Down
Loading