main
py 275 lines 13.4 KB
Raw
1 from pathlib import Path
2 import sys
3
4 import pytest
5
6
7 PROJECT_ROOT = Path(__file__).resolve().parents[1]
8 if str(PROJECT_ROOT) not in sys.path:
9 sys.path.insert(0, str(PROJECT_ROOT))
10
11
12 from helpers.file_browser import FileBrowser
13
14
15 def read(*parts: str) -> str:
16 return PROJECT_ROOT.joinpath(*parts).read_text(encoding="utf-8")
17
18
19 def test_file_browser_remember_last_directory_defaults_enabled() -> None:
20 settings_source = read("helpers", "settings.py")
21
22 assert "file_browser_remember_last_directory: bool" in settings_source
23 assert "file_browser_remember_last_directory=get_default_value(" in settings_source
24 assert '"file_browser_remember_last_directory",\n True,' in settings_source
25
26
27 def test_file_browser_editable_path_bar_and_remembered_directory_contract() -> None:
28 html = read("webui", "components", "modals", "file-browser", "file-browser.html")
29 store = read("webui", "components", "modals", "file-browser", "file-browser-store.js")
30 workdir_settings = read("webui", "components", "settings", "agent", "workdir.html")
31
32 assert 'class="path-navigator"' in html
33 assert 'class="nav-button back-button"' in html
34 assert 'class="text-button back-button"' not in html
35 assert ".nav-button:focus-visible" in html
36 assert ".nav-button .material-symbols-outlined" in html
37 assert 'class="nav-button-label">Up</span>' in html
38 assert "flex-direction: column;" in html
39 assert ".nav-button-label" in html
40 assert 'x-model="$store.fileBrowser.pathInput"' in html
41 assert '@submit.prevent="$store.fileBrowser.submitPath()"' in html
42 assert "Go to directory" in html
43 assert "$store.fileBrowser.pathError" in html
44
45 assert "FILE_BROWSER_LAST_DIRECTORY_STORAGE_KEY" in store
46 assert 'callJsonApi("settings_get", null)' in store
47 assert "file_browser_remember_last_directory" in store
48 assert "getRememberedDirectory()" in store
49 assert "rememberCurrentDirectory(this.browser.currentPath)" in store
50 assert "clearRememberedDirectory()" in store
51 assert "scheduleMountedDefaultLoad()" in store
52 assert 'this.browser.currentPath = "";' in store
53 assert 'this.browser.parentPath = "";' in store
54 assert 'const requestedPath = this.normalizeOpeningPath(path) || "$WORK_DIR";' in store
55 assert "`/get_work_dir_files?path=${encodeURIComponent(requestedPath)}`" in store
56 assert 'result.current_path || (requestedPath === "$WORK_DIR" ? "/a0" : requestedPath)' in store
57
58 explicit_path_index = store.index("const explicitPath = this.normalizeOpeningPath")
59 remembered_path_index = store.index("const rememberedPath = !explicitPath")
60 assert explicit_path_index < remembered_path_index
61
62 assert "Remember last file browser location" in workdir_settings
63 assert "$store.settings.settings.file_browser_remember_last_directory" in workdir_settings
64
65
66 def test_file_browser_compact_controls_and_narrow_layout_contract() -> None:
67 html = read("webui", "components", "modals", "file-browser", "file-browser.html")
68 dox = read("webui", "components", "modals", "file-browser", "AGENTS.md")
69
70 assert 'aria-label="New file"' in html
71 assert 'title="New file"' in html
72 assert 'aria-label="New folder"' in html
73 assert 'title="New folder"' in html
74 assert ">New File<" not in html
75 assert ">New Folder<" not in html
76 assert ".btn-new-item" in html
77 assert "width: 2.8rem;" in html
78 assert "height: 2.8rem;" in html
79 assert ".path-navigator {\n align-items: center;\n flex-direction: row;" in html
80 assert ".file-browser-toolbar {\n align-items: center;\n flex-direction: row;" in html
81 assert ".file-search-shell {\n flex: 1 1 auto;\n min-width: 0;\n width: auto;" in html
82 assert ".path-navigator .nav-button-label {\n display: none;" in html
83
84 assert "container: file-browser / inline-size;" in html
85 assert "@container file-browser (max-width: 620px)" in html
86 assert "grid-template-columns: 2.25rem minmax(0, 1fr) minmax(4.25rem, max-content) 8rem;" in html
87 assert ".file-cell-date,\n .file-date {\n display: none;" in html
88 assert ".file-cell-size,\n .file-size" not in html
89
90 assert "hiding the Modified date column" in dox
91 assert "New file and New folder controls icon-only" in dox
92
93
94 def test_file_browser_editor_picker_modes_have_primary_footer_actions() -> None:
95 html = read("webui", "components", "modals", "file-browser", "file-browser.html")
96 store = read("webui", "components", "modals", "file-browser", "file-browser-store.js")
97 dox = read("webui", "components", "modals", "file-browser", "AGENTS.md")
98
99 assert "PICKER_MODE_TEXT_OPEN" in store
100 assert "PICKER_MODE_SAVE_AS" in store
101 assert "openTextPicker" in store
102 assert "openSaveAsPicker" in store
103 assert 'new Set(["md", "txt"])' in store
104 assert "pickerSelectedFiles()" in store
105 assert "validatePickerFilename" in store
106 assert "handleFileNameClick(file = {})" in store
107 assert "fileSurfaceTarget(file) === \"editor\"" in store
108 assert "isEditorSurface(file = {})" in store
109 assert "canOpenInActionMenu(file = {})" in store
110
111 assert "file-browser-picker-actions" in html
112 assert "file-editor-open-action" in html
113 assert 'aria-label="Open in Editor"' in html
114 assert "picker-filename-input" in html
115 assert "Open Selected" in store
116 assert "Save Here" in store
117 assert "$store.fileBrowser.confirmPicker()" in html
118 assert "$store.fileBrowser.pickerSelectionLabel()" in html
119 assert "$store.fileBrowser.isPickerMode()" in html
120 assert "$store.fileBrowser.isTextOpenPicker()" in html
121 assert "picker-confirm-button" in html
122
123 assert "picker modes for Editor Open and Save As" in dox
124 assert "Markdown or plain text files" in dox
125 assert "Open in Editor action visible outside the overflow menu" in dox
126
127 editor_button_index = html.index("file-editor-open-action")
128 dropdown_menu_index = html.index('class="dropdown-menu file-actions-menu"')
129 assert editor_button_index < dropdown_menu_index
130 assert 'x-show="$store.fileBrowser.canOpenInActionMenu(file)"' in html
131
132
133 def test_file_browser_extract_and_editor_download_actions() -> None:
134 browser_html = read("webui", "components", "modals", "file-browser", "file-browser.html")
135 browser_store = read("webui", "components", "modals", "file-browser", "file-browser-store.js")
136 editor_html = read("plugins", "_editor", "webui", "editor-panel.html")
137 editor_store = read("plugins", "_editor", "webui", "editor-store.js")
138
139 assert 'x-show="!file.is_dir && $store.fileBrowser.isArchive(file.name)"' in browser_html
140 assert '$store.fileBrowser.extractArchive(file)' in browser_html
141 assert "ARCHIVE_SUFFIXES" in browser_store
142 assert 'fetchApi("/extract_work_dir_archive"' in browser_store
143 assert "async extractArchive(file = {})" in browser_store
144 assert "<span>Extract</span>" in browser_html
145 assert "downloadActiveFile()" in editor_store
146 assert "$store.editor.downloadActiveFile()" in editor_html
147 assert "<span>Download</span>" in editor_html
148
149
150 def test_file_browser_dropdown_escapes_scroll_container_and_header_is_opaque() -> None:
151 html = read("webui", "components", "modals", "file-browser", "file-browser.html")
152 store = read("webui", "components", "modals", "file-browser", "file-browser-store.js")
153
154 assert '<div class="files-list" @scroll="$store.fileBrowser.closeDropdown()">' in html
155 assert 'overflow: auto;' in html
156 assert 'x-teleport="body"' in html
157 assert 'class="dropdown-menu file-actions-menu"' in html
158 assert ':style="$store.fileBrowser.dropdownStyle"' in html
159 assert '@click.stop="$store.fileBrowser.toggleDropdown(file.path, $event.currentTarget)"' in html
160 assert "getDropdownStyle(triggerElement)" in store
161 assert 'position: "fixed"' in store
162 assert 'zIndex: "6000"' in store
163
164 assert "var(--secondary-bg)" not in html
165 assert "var(--border-color)" not in html
166 assert "var(--text-secondary)" not in html
167 assert "background: color-mix(in srgb, var(--color-panel) 88%, var(--color-background) 12%);" in html
168 assert "border-bottom: 1px solid var(--color-border);" in html
169
170
171 def test_file_browser_empty_api_path_uses_default_workdir_contract() -> None:
172 api_source = read("api", "get_work_dir_files.py")
173 api_dox = read("api", "get_work_dir_files.py.dox.md")
174
175 assert 'current_path = request.args.get("path", "") or "$WORK_DIR"' in api_source
176 assert 'current_path = "/a0"' in api_source
177 assert "Empty `path` requests and explicit `$WORK_DIR` requests resolve" in api_dox
178
179
180 def test_file_browser_is_registered_as_right_canvas_surface() -> None:
181 html = read("webui", "components", "modals", "file-browser", "file-browser.html")
182 store = read("webui", "components", "modals", "file-browser", "file-browser-store.js")
183 surfaces = read("webui", "js", "surfaces.js")
184 register = read("extensions", "webui", "right_canvas_register_surfaces", "register-files.js")
185 panel = read("extensions", "webui", "right-canvas-panels", "files-panel.html")
186 input_store = read("webui", "components", "chat", "input", "input-store.js")
187 welcome_store = read("webui", "components", "welcome", "welcome-store.js")
188
189 assert 'id: "files"' in surfaces
190 assert 'title: "Files"' in surfaces
191 assert 'modalPath: "modals/file-browser/file-browser.html"' in surfaces
192 assert 'await store.openSurface(payload.path || payload.filePath || payload.directory || "")' in surfaces
193 assert 'data-surface-id="files"' in html
194 assert 'data-surface-modal-path="modals/file-browser/file-browser.html"' in html
195 assert 'class="surface-modal file-browser-modal modal-no-backdrop"' in html
196 assert 'class="file-browser-modal-body"' in html
197 assert 'x-create="$store.fileBrowser.onMount($el, xAttrs($el) || {})"' in html
198 assert 'x-destroy="$store.fileBrowser.onUnmount(xAttrs($el) || {})"' in html
199 assert ".modal-inner.file-browser-modal" in html
200 assert "resize: both" in html
201 assert "openSurface(path" in store
202 assert "setupFloatingSurfaceModalChrome" in store
203 assert 'focusButtonClass: "file-browser-modal-focus-button"' in store
204 assert "beginSurfaceHandoff()" in store
205 assert "finishSurfaceHandoff()" in store
206 assert 'id: "files"' in register
207 assert "fileBrowserStore.openSurface" in register
208 assert 'data-surface-id="files"' in panel
209 assert 'path="modals/file-browser/file-browser.html" mode="canvas"' in panel
210 assert 'openLatestSurface("files"' in input_store
211 assert 'import { store as fileBrowserStore } from "/components/modals/file-browser/file-browser-store.js";' in welcome_store
212 assert "fileBrowserStore.open()" in welcome_store
213 assert "chatInputStore.browseFiles" not in welcome_store
214
215
216 def test_file_browser_reports_missing_directory(tmp_path: Path) -> None:
217 missing_directory = tmp_path / "missing"
218
219 result = FileBrowser().get_files(str(missing_directory))
220
221 assert result["entries"] == []
222 assert result["current_path"] == str(missing_directory)
223 assert result["error"] == "Directory not found"
224
225
226 def test_file_browser_moves_selected_items_without_overwriting_or_self_nesting(tmp_path: Path) -> None:
227 browser = FileBrowser()
228 browser.base_dir = tmp_path
229 source_file = tmp_path / "note.md"
230 source_folder = tmp_path / "skills"
231 destination = tmp_path / "archive"
232 source_file.write_text("hello", encoding="utf-8")
233 source_folder.mkdir()
234 destination.mkdir()
235
236 moved = browser.move_items(["note.md", "skills"], "archive")
237
238 assert moved == [str(destination / "note.md"), str(destination / "skills")]
239 assert (destination / "note.md").read_text(encoding="utf-8") == "hello"
240 assert (destination / "skills").is_dir()
241
242 collision = tmp_path / "collision.md"
243 collision.write_text("source", encoding="utf-8")
244 (destination / "collision.md").write_text("keep", encoding="utf-8")
245 with pytest.raises(FileExistsError, match="already exists"):
246 browser.move_items(["collision.md"], "archive")
247 assert collision.read_text(encoding="utf-8") == "source"
248 assert (destination / "collision.md").read_text(encoding="utf-8") == "keep"
249
250 nested = destination / "skills" / "nested"
251 nested.mkdir()
252 with pytest.raises(ValueError, match="cannot be moved into itself"):
253 browser.move_items(["archive/skills"], "archive/skills/nested")
254
255
256 def test_file_browser_drag_and_drop_contract() -> None:
257 html = read("webui", "components", "modals", "file-browser", "file-browser.html")
258 store = read("webui", "components", "modals", "file-browser", "file-browser-store.js")
259 attachments = read("webui", "components", "chat", "attachments", "attachmentsStore.js")
260 api = read("api", "rename_work_dir_file.py")
261
262 assert ':draggable="!$store.fileBrowser.isPickerMode() && !$store.fileBrowser.isBulkBusy"' in html
263 assert "$store.fileBrowser.dropItems(file.path, file.name, $event)" in html
264 assert "$store.fileBrowser.dropItems($store.fileBrowser.browser.parentPath, 'parent folder', $event)" in html
265 start_drag = store[store.index(" startDrag("):store.index(" isDraggingPath(")]
266 assert "this.clearSelection()" not in start_drag
267 assert "file.selected = true" not in start_drag
268 assert ": [file.path]" in start_drag
269 assert "decorateEntries(data.data?.entries || [], selectedPaths)" in store
270 assert "application/x-agent-zero-files" in store
271 assert 'action: "move"' in store
272 assert 'fetchApi("/rename_work_dir_file"' in store
273 assert 'if action == "move":' in api
274 assert 'isExternalFileDrag(event)' in attachments
275 assert 'includes("Files")' in attachments