Skip to content
Merged
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
10 changes: 9 additions & 1 deletion src/chrome/src/agent/agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -17512,7 +17512,9 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
return await downloadResourceFromPage(tabId, args);
}
if (name === 'download_files' || name === 'download_file') {
if (args.url && !args.urls) args.urls = [args.url];
if (args.url && (!Array.isArray(args.urls) || args.urls.length === 0)) {
args.urls = [args.url];
}
return await downloadFiles(args);
}

Expand Down Expand Up @@ -18828,6 +18830,12 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d

if (name === 'upload_file') {
args = args || {};
// Some providers materialize omitted optional schema properties as empty
// strings. Treat those placeholders as absent so a valid downloadId does
// not conflict with a nonexistent user attachment or local path.
for (const key of ['selector', 'attachmentId', 'filePath']) {
if (typeof args[key] === 'string' && !args[key].trim()) delete args[key];
}
const compactUpload = (
executionContext?.promptTier || this._resolvePromptTier()
) === 'compact';
Expand Down
10 changes: 9 additions & 1 deletion src/firefox/src/agent/agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -15331,13 +15331,21 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
return await downloadResourceFromPage(tabId, args);
}
if (name === 'download_files') {
if (args.url && !args.urls) args.urls = [args.url];
if (args.url && (!Array.isArray(args.urls) || args.urls.length === 0)) {
args.urls = [args.url];
}
return await downloadFiles(args);
}
if (name === 'upload_file') {
const UPLOAD_MAX_BYTES = 25 * 1024 * 1024;
try {
args = args || {};
// Some providers materialize omitted optional schema properties as empty
// strings. Treat those placeholders as absent so a valid downloadId does
// not conflict with a nonexistent user attachment or local path.
for (const key of ['selector', 'attachmentId', 'filePath']) {
if (typeof args[key] === 'string' && !args[key].trim()) delete args[key];
}
const compactUpload = (
executionContext?.promptTier || this._resolvePromptTier()
) === 'compact';
Expand Down
75 changes: 74 additions & 1 deletion test/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -54565,6 +54565,72 @@ test('download_files digest echoes safe downloadIds but never the filename (chro
}
});

test('download_files falls back to singular url when providers emit an empty urls placeholder', async () => {
const originalChrome = globalThis.chrome;
const originalBrowser = globalThis.browser;
const url = 'https://example.com/webbrain-firefox.zip';
try {
let chromeDownloadOptions = null;
globalThis.chrome = {
runtime: { lastError: null },
downloads: {
download(options, cb) {
chromeDownloadOptions = options;
cb(468);
},
search(_query, cb) {
cb([{
id: 468,
filename: '/Users/test/Downloads/webbrain-firefox.zip',
state: 'complete',
bytesReceived: 10,
totalBytes: 10,
}]);
},
},
};
const chromeArgs = { url, urls: [], filename: 'webbrain-firefox.zip' };
const chromeResult = await new AgentCh({}).executeTool(42, 'download_files', chromeArgs);
assert.equal(chromeResult.success, true);
assert.deepEqual(chromeArgs.urls, [url]);
assert.equal(chromeDownloadOptions.url, url);

let firefoxDownloadOptions = null;
globalThis.browser = {
storage: {
local: {
async get() { return { downloadDirectory: '' }; },
},
},
downloads: {
async download(options) {
firefoxDownloadOptions = options;
return 469;
},
async search() {
return [{
id: 469,
filename: '/Users/test/Downloads/webbrain-firefox.zip',
state: 'complete',
bytesReceived: 10,
totalBytes: 10,
}];
},
},
};
const firefoxArgs = { url, urls: [], filename: 'webbrain-firefox.zip' };
const firefoxResult = await new AgentFx({}).executeTool(42, 'download_files', firefoxArgs);
assert.equal(firefoxResult.success, true);
assert.deepEqual(firefoxArgs.urls, [url]);
assert.equal(firefoxDownloadOptions.url, url);
} finally {
if (originalChrome === undefined) delete globalThis.chrome;
else globalThis.chrome = originalChrome;
if (originalBrowser === undefined) delete globalThis.browser;
else globalThis.browser = originalBrowser;
}
});

test('download_files treats interrupted browser downloads as failed (chrome & firefox)', async () => {
const originalChrome = globalThis.chrome;
const originalBrowser = globalThis.browser;
Expand Down Expand Up @@ -55221,7 +55287,9 @@ test('upload_file prefers a valid downloadId and falls back to filePath for an i
cdpClientCh.getFileInputFiles = async () => [];
const consumed = await agent.executeTool(42, 'upload_file', {
selector: 'input[type=file]',
attachmentId: '',
downloadId: 9123,
filePath: '',
});
assert.equal(consumed.success, true);
assert.equal(consumed.attachmentState, 'page_consumed');
Expand Down Expand Up @@ -55699,7 +55767,12 @@ test('upload_file (firefox) re-fetches downloadId with manual redirect handling
};

const agent = new AgentFx({});
const args = { selector: 'input[type=file]', downloadId: 8123 };
const args = {
selector: 'input[type=file]',
attachmentId: '',
downloadId: 8123,
filePath: '',
};
const result = await agent.executeTool(42, 'upload_file', args);

assert.equal(result.success, true);
Expand Down
Loading