Merge pull request #6935 from redpersongpt/codex/email-paperclip-files

fix(email): retain files in attachment and inline-image pickers
This commit is contained in:
Alexandre Teixeira
2026-10-30 19:02:22 +00:00
committed by GitHub
3 changed files with 66 additions and 3 deletions
+2 -2
View File
@@ -5232,7 +5232,7 @@ import { attachColorPicker } from './colorPicker.js?v=20260910eyedropper1';
} }
async function _handleAttachUpload(e) { async function _handleAttachUpload(e) {
const files = e.target.files; const files = Array.from(e.target.files || []);
e.target.value = ''; // reset for next upload e.target.value = ''; // reset for next upload
await _uploadComposeFiles(files); await _uploadComposeFiles(files);
} }
@@ -5963,7 +5963,7 @@ import { attachColorPicker } from './colorPicker.js?v=20260910eyedropper1';
} }
async function _handleMarkdownImageUpload(e) { async function _handleMarkdownImageUpload(e) {
const files = e.target.files; const files = Array.from(e.target.files || []);
e.target.value = ''; e.target.value = '';
await _uploadMarkdownImages(files); await _uploadMarkdownImages(files);
} }
@@ -0,0 +1,61 @@
"""The native FileList must survive resetting the compose file input."""
import json
import shutil
import subprocess
from pathlib import Path
import pytest
from tests.helpers.document_source import function_body
@pytest.mark.parametrize("handler,uploader", [("_handleAttachUpload", "_uploadComposeFiles"), ("_handleMarkdownImageUpload", "_uploadMarkdownImages")])
@pytest.mark.parametrize("selections", [[["one.png"]], [["one.png", "two.png"]], [["one.png"], ["one.png"]], [[]]])
def test_compose_file_selection_survives_reset(tmp_path, selections, handler, uploader):
if not shutil.which("node"):
pytest.skip("node not installed")
repo = Path(__file__).resolve().parents[1]
for name, icon in [("one.png", "icon-192.png"), ("two.png", "icon-512.png")]:
shutil.copyfile(repo / "static/icons" / icon, tmp_path / name)
script = r"""
const fs=require('node:fs'),assert=require('node:assert/strict'),crypto=require('node:crypto');
let chromium;try{({chromium}=require('playwright'));}catch(e){if(e.code==='MODULE_NOT_FOUND')process.exit(77);throw e;}
(async()=>{
const browser=await chromium.launch({headless:true});
try{
const page=await browser.newPage();const errors=[];page.on('pageerror',e=>errors.push(e.message));
await page.goto(process.env.ODYSSEUS_TEST_STATIC_ORIGIN+'/static/js/storage.js');
await page.setContent('<input type="file" multiple aria-label="Compose attachments">');
await page.addScriptTag({content:`
window.uploads=[];window.finished=0;
async function __UPLOADER__(files){
for(const file of files){
const hash=Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256',await file.arrayBuffer()))).map(x=>x.toString(16).padStart(2,'0')).join('');
window.uploads.push({name:file.name,hash});
}
}
__HANDLER__
document.querySelector('input').addEventListener('change',async event=>{
await __HANDLER_NAME__(event);window.finished++;
});
`});
const selections=JSON.parse(process.argv[1]),root=process.argv[2],expected=[];
for(const [index,names] of selections.entries()){
await page.getByLabel('Compose attachments').setInputFiles(names.map(name=>root+'/'+name));
await page.waitForFunction(n=>window.finished===n,index+1);
expected.push(...names.map(name=>({name,hash:crypto.createHash('sha256').update(fs.readFileSync(root+'/'+name)).digest('hex')})));
assert.deepEqual(await page.evaluate(()=>window.uploads),expected);
assert.equal(await page.getByLabel('Compose attachments').inputValue(),'');
assert.equal(await page.getByLabel('Compose attachments').evaluate(el=>el.files.length),0);
}
assert.deepEqual(errors,[]);console.log(JSON.stringify({files:expected.length}));
}finally{await browser.close();}
})().catch(e=>{console.error(e);process.exitCode=1;});
""".replace("__HANDLER__", function_body(handler)).replace("__HANDLER_NAME__", handler).replace("__UPLOADER__", uploader)
result = subprocess.run(["node", "-e", script, json.dumps(selections), str(tmp_path)],
cwd=Path(__file__).resolve().parents[1], capture_output=True,
text=True, timeout=30)
if result.returncode == 77:
pytest.skip("playwright not installed")
assert result.returncode == 0, result.stdout + result.stderr
assert json.loads(result.stdout)["files"] == sum(map(len, selections))
+3 -1
View File
@@ -144,10 +144,12 @@ def test_editor_agent_endpoint_reports_scoped_tools():
async def _route_contract(monkeypatch, message, *, attachment_meta=(), form=None, messages_out=None): async def _route_contract(monkeypatch, message, *, attachment_meta=(), form=None, messages_out=None):
from routes import chat_routes from routes import chat_routes
from src import tool_security from src import tool_security, tool_utils
from tests.test_foreground_model_routing import _RouteRequest, _chat_stream_endpoint from tests.test_foreground_model_routing import _RouteRequest, _chat_stream_endpoint
with monkeypatch.context() as route_patch: with monkeypatch.context() as route_patch:
# This built-in catalog fixture has no configured external MCP servers.
route_patch.setattr(tool_utils, "get_mcp_manager", lambda: None)
endpoint = _chat_stream_endpoint( endpoint = _chat_stream_endpoint(
route_patch, "agent", {}, route_patch, "agent", {},
session_model="odysseus-qwen3.5-tools-pre-heretic", session_model="odysseus-qwen3.5-tools-pre-heretic",