main
py 732 lines 35.5 KB
Raw
1 from __future__ import annotations
2
3 from pathlib import Path
4
5
6 PROJECT_ROOT = Path(__file__).resolve().parents[1]
7
8
9 def read(*parts: str) -> str:
10 return (PROJECT_ROOT.joinpath(*parts)).read_text(encoding="utf-8")
11
12
13 def test_modals_are_generic_and_surfaces_own_live_surface_paths():
14 modals_js = read("webui", "js", "modals.js")
15 modals_css = read("webui", "css", "modals.css")
16 surfaces_js = read("webui", "js", "surfaces.js")
17 surfaces_css = read("webui", "css", "surfaces.css")
18
19 for forbidden in (
20 "right-canvas-store",
21 "/plugins/_browser",
22 "/plugins/_office",
23 "/plugins/_desktop",
24 "SINGLE_VISIBLE_MODAL_SURFACE_PATHS",
25 "data-canvas",
26 "surface-window",
27 ):
28 assert forbidden not in modals_js
29
30 assert "modalStack" in modals_js
31 assert 'const backdrop = document.createElement("div")' in modals_js
32 assert "backdrop.style.display" in modals_js
33 assert "modalSurfaceMetadata" not in modals_js
34 assert "modal-content-loaded" in modals_js
35 assert "modal.returnFocus = returnFocus" in modals_js
36 assert "modal.returnFocus?.isConnected" in modals_js
37 assert ".surface-floating" not in modals_css
38 assert ".surface-switcher" not in modals_css
39
40 assert "CORE_SURFACES" in surfaces_js
41 assert "modalSurfaceMetadata" in surfaces_js
42 assert "closeSurfaceGroupModals" in surfaces_js
43 assert 'id: "browser"' in surfaces_js
44 assert 'id: "desktop"' in surfaces_js
45 assert 'id: "editor"' in surfaces_js
46 assert "/plugins/_browser/webui/main.html" in surfaces_js
47 assert "/plugins/_desktop/webui/main.html" in surfaces_js
48 assert "/plugins/_editor/webui/main.html" in surfaces_js
49 assert "LEGACY_SURFACE_IDS" in surfaces_js
50 assert '["office", "desktop"]' in surfaces_js
51 assert "htmlDataset.surfaceId" in surfaces_js
52 assert "htmlDataset.canvasSurface" in surfaces_js
53 assert ".surface-modal" in surfaces_css
54 assert ".surface-floating" in surfaces_css
55 assert ".surface-resize-handle" in surfaces_css
56 assert ".surface-switcher" in surfaces_css
57 assert "surface-window" not in surfaces_js + surfaces_css
58
59
60 def test_right_canvas_uses_desktop_surface_id_and_migrates_legacy_office_state():
61 canvas_store = read("webui", "components", "canvas", "right-canvas-store.js")
62 desktop_register = read(
63 "plugins",
64 "_desktop",
65 "extensions",
66 "webui",
67 "surfaces_register",
68 "register-desktop.js",
69 )
70 desktop_panel = read(
71 "plugins",
72 "_desktop",
73 "extensions",
74 "webui",
75 "right-canvas-panels",
76 "desktop-panel.html",
77 )
78 desktop_new_menu = read(
79 "plugins",
80 "_desktop",
81 "extensions",
82 "webui",
83 "right-canvas-toolbar-start",
84 "desktop-new-menu.html",
85 )
86 right_canvas_css = read("webui", "components", "canvas", "right-canvas.css")
87 desktop_web_panel = read("plugins", "_desktop", "webui", "desktop-panel.html")
88 editor_register = read(
89 "plugins",
90 "_editor",
91 "extensions",
92 "webui",
93 "right_canvas_register_surfaces",
94 "register-editor.js",
95 )
96 editor_panel = read(
97 "plugins",
98 "_editor",
99 "extensions",
100 "webui",
101 "right-canvas-panels",
102 "editor-panel.html",
103 )
104 editor_main = read("plugins", "_editor", "webui", "main.html")
105 editor_web_panel = read("plugins", "_editor", "webui", "editor-panel.html")
106 editor_store = read("plugins", "_editor", "webui", "editor-store.js")
107 editor_preview = read("plugins", "_editor", "webui", "editor-preview.js")
108 safe_markdown = read("webui", "js", "safe-markdown.js")
109
110 assert 'await callJsExtensions("surfaces_register", this);' in canvas_store
111 assert 'await callJsExtensions("right_canvas_register_surfaces", this);' in canvas_store
112 assert "migratePersistedSurfaceState" in canvas_store
113 assert "normalizeSurfaceId" in canvas_store
114 assert "const saved = migratePersistedSurfaceState(JSON.parse" in canvas_store
115 assert 'id: "desktop"' in desktop_register
116 assert 'modalPath: "/plugins/_desktop/webui/main.html"' in desktop_register
117 assert 'id: "editor"' in editor_register
118 assert 'title: "Editor"' in editor_register
119 assert 'order: 30' in editor_register
120 assert 'modalPath: "/plugins/_editor/webui/main.html"' in editor_register
121 assert 'data-surface-id="desktop"' in desktop_panel
122 assert "isSurfaceVisible('desktop')" in desktop_panel
123 assert 'data-surface-id="editor"' in editor_panel
124 assert "isSurfaceVisible('editor')" in editor_panel
125 assert 'data-surface-id="editor"' in editor_main
126 assert 'data-surface-modal-path="/plugins/_editor/webui/main.html"' in editor_main
127 assert "editor-source-editor" in editor_web_panel
128 assert "data-editor-source" in editor_web_panel
129 assert "editor-tabs" in editor_web_panel
130 assert "editor-new-tab" in editor_web_panel
131 assert 'aria-label="New Markdown"' in editor_web_panel
132 assert "editor-close-confirm" in editor_web_panel
133 assert "Save & Close" in editor_web_panel
134 assert "Close All" in editor_web_panel
135 assert "editor-document-header" not in editor_web_panel
136 assert "editor-document-save-button" not in editor_web_panel
137 assert "data-editor-ace" in editor_web_panel
138 assert "data-editor-preview" in editor_web_panel
139 assert "data-editor-preview-source" in editor_web_panel
140 assert "editor-mode-toggle" in editor_web_panel
141 assert "editor-search-bar" in editor_web_panel
142 assert "editor-preview-title" in editor_web_panel
143 assert "editor-preview-page-editor" in editor_web_panel
144 assert "editor-table-wrap" in editor_web_panel
145 assert "editor-empty" in editor_web_panel
146 assert "runNewMenuAction('open')" in editor_web_panel
147 assert "runNewMenuAction('markdown')" in editor_web_panel
148 assert "closeAllFiles" in editor_store
149 assert "confirmPendingClose" in editor_store
150 assert "ensureInitialMarkdownFile" not in editor_store
151 assert "_initialCreatePromise" not in editor_store
152 assert "startPreviewEdit" in editor_store
153 assert "applyPreviewEdit" in editor_store
154 assert "previewEditDirty" in editor_store
155 assert "replacePageMarkdown" in editor_store
156 assert "enhanceTaskLists" in editor_store
157 assert "togglePreviewTask" in editor_store
158 assert 'input[type="checkbox"]' in editor_store
159 assert "renderEditorPreviewMarkdown" in editor_store
160 assert "buildMarkdownPages" in editor_store
161 assert "PAGE_HEADING_RE" not in editor_preview
162 assert "markdown: source" in editor_preview
163 assert "hydrateActiveSession" in editor_store
164 assert "refreshSourceEditorLayout" in editor_store
165 assert "editor.resize?.(true)" in editor_store
166 assert "openSearch" in editor_store
167 assert "handlePreviewClick" in editor_store
168 assert "ace.edit" in editor_store
169 assert "showGutter: false" in editor_store
170 assert "globalThis.confirm" not in editor_store
171 assert ".editor-toolbar" in editor_web_panel
172 assert "overflow: visible;" in editor_web_panel
173 assert "z-index: 10000;" in editor_web_panel
174 assert "renderSafeMarkdown" in editor_preview
175 assert "prepareFootnotes" in editor_preview
176 assert "resolveDocumentRelativePath" in editor_preview
177 assert "allowDataImages: true" in editor_preview
178 assert "allowLatex: true" in editor_preview
179 assert "html = sanitizeHtml(html, options);" in safe_markdown
180 assert "right-canvas-desktop-actions" in desktop_new_menu
181 assert "isSurfaceActive('desktop')" in desktop_new_menu
182 assert "runNewMenuAction('writer')" in desktop_new_menu
183 assert "runNewMenuAction('spreadsheet')" in desktop_new_menu
184 assert "runNewMenuAction('presentation')" in desktop_new_menu
185 assert "runNewMenuAction('markdown')" not in desktop_new_menu
186 assert ".right-canvas-header" in right_canvas_css
187 assert "overflow: visible;" in right_canvas_css
188 assert ".right-canvas-toolbar" in right_canvas_css
189 assert ".right-canvas-desktop-actions .office-new-menu" in desktop_web_panel
190 assert "z-index: 4000;" in desktop_web_panel
191 assert not (PROJECT_ROOT / "plugins" / "_office" / "extensions" / "webui" / "right_canvas_register_surfaces" / "register-office.js").exists()
192 assert not (PROJECT_ROOT / "plugins" / "_office" / "extensions" / "webui" / "right-canvas-panels" / "office-panel.html").exists()
193
194
195 def test_browser_surface_restores_focus_mode_chrome():
196 browser_store = read("plugins", "_browser", "webui", "browser-store.js")
197 browser_panel = read("plugins", "_browser", "webui", "browser-panel.html")
198
199 assert "browser-modal-focus-button" in browser_store
200 assert "is-focus-mode" in browser_store
201 assert "fullscreen_exit" in browser_store
202 assert "Focus mode" in browser_store
203 assert "Restore size" in browser_store
204 assert ".modal-inner.browser-modal.is-focus-mode" in browser_panel
205
206
207 def test_files_and_editor_surface_modals_have_draggable_focus_chrome():
208 surfaces_js = read("webui", "js", "surfaces.js")
209 surfaces_css = read("webui", "css", "surfaces.css")
210 file_store = read("webui", "components", "modals", "file-browser", "file-browser-store.js")
211 file_modal = read("webui", "components", "modals", "file-browser", "file-browser.html")
212 editor_store = read("plugins", "_editor", "webui", "editor-store.js")
213 editor_panel = read("plugins", "_editor", "webui", "editor-panel.html")
214
215 assert "setupFloatingSurfaceModalChrome" in surfaces_js
216 assert "is-draggable-surface-modal" in surfaces_js
217 assert "surface-modal-focus-button" in surfaces_js
218 assert "fullscreen_exit" in surfaces_js
219 assert "Focus mode" in surfaces_js
220 assert "Restore size" in surfaces_js
221 assert ".modal-inner.surface-modal.is-draggable-surface-modal .modal-header" in surfaces_css
222 assert "cursor: move" in surfaces_css
223 assert ".surface-modal-focus-button.is-active" in surfaces_css
224
225 assert "setupFloatingSurfaceModalChrome" in file_store
226 assert 'focusButtonClass: "file-browser-modal-focus-button"' in file_store
227 assert ".modal-inner.file-browser-modal" in file_modal
228 assert "resize: both" in file_modal
229
230 assert "setupFloatingSurfaceModalChrome" in editor_store
231 assert 'focusButtonClass: "editor-modal-focus-button"' in editor_store
232 assert "onBoundsChange: () => this.refreshSourceEditorLayout()" in editor_store
233 assert "editor.resize?.(true)" in editor_store
234 assert ".modal-inner.editor-modal.is-focus-mode" in editor_panel
235
236
237 def test_office_frontend_is_document_only_and_does_not_import_browser_or_desktop_runtime_code():
238 office_store = read("plugins", "_office", "webui", "office-store.js")
239 office_panel = read("plugins", "_office", "webui", "office-panel.html")
240 office_modal = read("plugins", "_office", "webui", "main.html")
241
242 assert "/plugins/_browser" not in office_store
243 assert "right-canvas-store" not in office_store
244 assert "handleUrlIntent" not in office_store
245 assert "ensureDesktopSession" not in office_store
246 assert "desktop_save" not in office_store
247 assert "desktop_sync" not in office_store
248 assert "desktop_state" not in office_store
249 assert "desktop_shutdown" not in office_store
250 assert "Xpra" not in office_store
251 assert "xpra" not in office_store
252 assert "data-office-desktop-host" not in office_panel
253 assert "office-desktop-frame" not in office_panel
254 assert "Restart Desktop" not in office_panel
255 assert "data-surface-id" not in office_modal
256 assert "modal-no-backdrop" not in office_modal
257 assert "data-canvas-surface" not in office_modal
258
259 assert "office-source-editor" not in office_panel
260 assert "data-office-source" not in office_panel
261 assert "office-document-header" not in office_panel
262 assert "runNewMenuAction('markdown')" not in office_panel
263 assert 'data-office-new-action="markdown"' not in office_store
264 assert "openRenameModal" not in office_store
265 assert "office_save" not in office_store
266 assert 'callOffice("renamed"' not in office_store
267 assert "data-office-source" not in office_store
268 assert "office_input" not in office_store
269 assert "requires_desktop" in office_store
270 assert "openSurface(\"desktop\"" not in office_store
271
272
273 def test_desktop_plugin_owns_routes_runtime_surface_and_state_paths():
274 desktop_plugin = PROJECT_ROOT / "plugins" / "_desktop"
275 assert (desktop_plugin / "plugin.yaml").exists()
276 assert (desktop_plugin / "api" / "desktop_session.py").exists()
277 assert (desktop_plugin / "helpers" / "desktop_session.py").exists()
278 assert (desktop_plugin / "helpers" / "desktop_state.py").exists()
279 assert (desktop_plugin / "skills" / "linux-desktop" / "scripts" / "desktopctl.sh").exists()
280
281 desktop_startup = read("plugins", "_desktop", "extensions", "python", "startup_migration", "_20_desktop_routes.py")
282 desktop_api = read("plugins", "_desktop", "api", "desktop_session.py")
283 desktop_session = read("plugins", "_desktop", "helpers", "desktop_session.py")
284 desktop_state = read("plugins", "_desktop", "helpers", "desktop_state.py")
285 desktop_store = read("plugins", "_desktop", "webui", "desktop-store.js")
286 desktop_main = read("plugins", "_desktop", "webui", "main.html")
287 desktop_web_panel = read("plugins", "_desktop", "webui", "desktop-panel.html")
288
289 assert "virtual_desktop_routes.install_route_hooks()" in desktop_startup
290 assert 'action in {"open_document", "document"}' in desktop_api
291 assert "document_store.EDITOR_TEXT_EXTENSIONS" in desktop_api
292 assert "Text documents use the Editor surface." in desktop_api
293 assert "return self._open_markdown(doc, input, request)" not in desktop_api
294 assert "markdown_sessions" not in desktop_api
295 assert '"status": desktop.get("status") or {}' in desktop_api
296 assert 'callJsonApi("/plugins/_desktop/desktop_session"' in desktop_store
297 assert 'callDesktop("open_document"' in desktop_store
298 assert 'callOffice("create"' in desktop_store
299 assert "open_in_desktop: isOfficialExtension(fmt)" in desktop_store
300 assert "DESKTOP_RUNTIME_INSTALL_MESSAGE" in desktop_store
301 assert "openDesktopWhenRuntimeReady" in desktop_store
302 assert "isDesktopRuntimeInstalling" in desktop_store
303 assert "_desktopDisplaySizes: {}" in desktop_store
304 assert "desktopDisplaySizeForToken(token" in desktop_store
305 assert "rememberDesktopDisplaySize(token" in desktop_store
306 assert "options.displaySize || this.desktopDisplaySizeForToken(token)" in desktop_store
307 assert "result?.width || width" in desktop_store
308 assert "canvas.width = normalizedWidth" in desktop_store
309 assert "canvas.height = normalizedHeight" in desktop_store
310 assert "canvas?.clientWidth || canvas?.width" in desktop_store
311 assert "overflow: auto !important;" in desktop_store
312 assert "Installing Agent Zero Desktop runtime dependencies" in desktop_session
313 assert "normalize_desktop_display_size" in desktop_session
314 assert "__a0XpraOffsetWarnPatched" in desktop_store
315 assert "window does not fit in canvas, offsets" in desktop_store
316 assert "decode error packet" in desktop_store
317 assert 'data-surface-id="desktop"' in desktop_main
318 assert "virtual_desktop.session_url" in desktop_session
319 assert 'owner="desktop"' in desktop_session
320 assert 'STATE_DIR = Path(files.get_abs_path("usr", "plugins", PLUGIN_NAME))' in desktop_session
321 assert 'STATE_DIR = BASE_DIR / "usr" / "plugins" / PLUGIN_NAME' in desktop_state
322 assert "> x-component > div[x-data] > .office-panel" in desktop_web_panel
323 assert ".office-state-line > span:not(.material-symbols-outlined)" in desktop_web_panel
324
325 assert not (PROJECT_ROOT / "plugins" / "_office" / "helpers" / "desktop_state.py").exists()
326 assert not (PROJECT_ROOT / "plugins" / "_office" / "helpers" / "libreoffice_desktop.py").exists()
327 assert not (PROJECT_ROOT / "plugins" / "_office" / "helpers" / "libreoffice_desktop_routes.py").exists()
328 assert not (PROJECT_ROOT / "plugins" / "_office" / "assets" / "desktop").exists()
329
330
331 def test_plugin_owned_runtime_state_paths_are_declared():
332 office_documents = read("plugins", "_office", "helpers", "document_store.py")
333 browser_playwright = read("plugins", "_browser", "helpers", "playwright.py")
334 browser_extensions = read("plugins", "_browser", "helpers", "extension_manager.py")
335 docker_playwright = read("docker", "run", "fs", "ins", "install_playwright.sh")
336
337 assert 'PLUGIN_NAME = "_office"' in office_documents
338 assert 'STATE_DIR = Path(files.get_abs_path("usr", "plugins", PLUGIN_NAME, "documents"))' in office_documents
339 assert 'PLAYWRIGHT_CACHE_DIR = ("tmp", "playwright")' in browser_playwright
340 assert '"usr", "plugins", "_browser", "playwright"' in browser_playwright
341 assert "Path(files.get_abs_path(*PLAYWRIGHT_CACHE_DIR))" in browser_playwright
342 assert "find_playwright_binary(_primary_cache_dir())" in browser_playwright
343 assert "Path(files.get_abs_path(*EXTENSIONS_ROOT_DIR))" in browser_extensions
344 assert "PLAYWRIGHT_BROWSERS_PATH=/a0/tmp/playwright" in docker_playwright
345
346
347 def test_office_artifacts_only_open_desktop_from_explicit_document_ui_requests():
348 auto_open = read(
349 "plugins",
350 "_office",
351 "extensions",
352 "webui",
353 "set_messages_after_loop",
354 "auto-open-document-results.js",
355 )
356 document_actions = read("plugins", "_office", "extensions", "webui", "lib", "document-actions.js")
357 document_handler = read(
358 "plugins",
359 "_office",
360 "extensions",
361 "webui",
362 "get_tool_message_handler",
363 "office-artifact-handler.js",
364 )
365 response_cards = read(
366 "plugins",
367 "_office",
368 "extensions",
369 "webui",
370 "set_messages_after_loop",
371 "document-response-file-cards.js",
372 )
373 messages_css = read("webui", "css", "messages.css")
374 document_tool = read("plugins", "_office", "tools", "office_artifact.py")
375 office_api = read("plugins", "_office", "api", "office_session.py")
376 editor_sync = read(
377 "plugins",
378 "_editor",
379 "extensions",
380 "webui",
381 "set_messages_after_loop",
382 "sync-text-editor-results.js",
383 )
384
385 assert 'openSurface(surfaceForDocument' in auto_open
386 assert 'return "desktop";' in auto_open
387 assert "isExplicitDocumentUiRequest(payload)" in auto_open
388 assert 'action === "open"' in auto_open
389 assert "open_in_canvas" in auto_open
390 assert "open_in_desktop" in auto_open
391 assert 'surfaces.open("desktop"' not in auto_open
392 assert "rightCanvas.open" not in auto_open
393 assert "globalThis.Alpine" not in auto_open
394 assert "syncDocumentResultsIntoOpenSurfaces" in auto_open
395 assert "isOfficeCanvas" not in auto_open
396 assert "officeStore" in auto_open
397 assert "desktopStore" in auto_open
398 assert "store?.previewEditDirty" in auto_open
399 assert "syncOpenDesktopCanvas" in auto_open
400 assert "syncOpenOfficeModal" in auto_open
401 assert "isDesktopSurfaceOpen" in auto_open
402 assert "function documentTarget(payload = {}, document = {})" in auto_open
403 assert 'toolName !== "office_artifact"' in auto_open
404 assert "syncTextEditorResultsIntoOpenEditor" in editor_sync
405 assert 'toolName(payload) !== "text_editor"' in editor_sync
406 assert 'return ["write", "patch"].includes(action);' in editor_sync
407 assert "if (!context?.results?.length) return;" in editor_sync
408 assert "if (context.historyEmpty && !explicitOpen) continue;" in editor_sync
409 assert "syncOpenEditorSurface" in editor_sync
410 assert "isEditorSurfaceOpen" in editor_sync
411 assert "context_id" in editor_sync
412 assert "ctxid" in editor_sync
413 assert "void syncOpenDocumentSurfaces(target);" in auto_open
414 assert "void syncOpenDocumentSurfaces({ path, file_id: fileId });" not in auto_open
415 assert "editorStore" not in auto_open
416 assert "text_editor" not in auto_open
417 assert "hasSameDocument" in auto_open
418 assert 'source: "tool-result-sync"' in auto_open
419 assert '".modal .office-panel"' not in auto_open
420 assert "normalizeDocumentMetadata" in document_actions
421 assert "buildDocumentFileCard" in document_actions
422 assert "document-file-card" in document_actions
423 assert "buildDocumentFileCard" not in document_handler
424 assert "buildDocumentFileActionButtons" not in document_handler
425 assert "document-file-card-wrapper" not in document_handler
426 assert "message-document-artifact" not in document_handler
427 assert "actionButtons: []" in document_handler
428 assert "injectDocumentCardsIntoFinalResponses" in response_cards
429 assert "buildDocumentFileCard" in response_cards
430 assert "buildDocumentFileActionButtons" in response_cards
431 assert "message-agent-response" in response_cards
432 assert "document-response-file-cards" in response_cards
433 assert "document-response-file-action" in response_cards
434 assert "RESPONSE_CARD_ACTIONS" in response_cards
435 assert "documentIdentityKey" in response_cards
436 assert "uniqueByDocument" in response_cards
437 assert "PENDING_TTL_MS" in response_cards
438 assert "pendingContextId" in response_cards
439 assert "globalThis.getContext" in response_cards
440 assert "prunePendingDocuments" in response_cards
441 assert "wrapper.dataset.documents" in response_cards
442 assert "refreshResponseFileActions" in response_cards
443 assert "parseStoredDocuments" in response_cards
444 assert "openDocumentInDesktop" in document_actions
445 assert "openDocumentInEditor" not in document_actions
446 assert "openOfficeArtifact" in document_actions
447 assert "openDocumentArtifact" not in document_actions
448 assert 'await openSurface("editor"' not in document_actions
449 assert "await openDocumentInDesktop(document);" in document_actions
450 assert 'ensureModalOpen("/plugins/_office/webui/main.html")' not in document_actions
451 assert 'ensureModalOpen("/plugins/_office/webui/main.html")' not in auto_open
452 assert "Open in canvas" in document_actions
453 assert "Copy path" not in document_actions
454 assert "copyToClipboard" not in document_actions
455 assert "Details" not in document_handler
456 assert "Details" not in response_cards
457 assert "/api/download_work_dir_file" in document_actions
458 assert 'openSurface("desktop"' in document_actions
459 assert 'openSurface("editor"' not in document_actions
460 assert "Open in canvas with Writer" in document_actions
461 assert "Open in canvas with Calc" in document_actions
462 assert "Open in canvas with Impress" in document_actions
463 assert 'const DESKTOP_FORMATS = ["odt", "ods", "odp", "docx", "xlsx", "pptx"]' in document_actions
464 assert ".document-file-card" in messages_css
465 assert ".document-response-file-cards" in messages_css
466 assert ".document-file-action-label" not in messages_css
467 assert ".process-step-detail-content.document-file-card-wrapper" not in messages_css
468 assert "open_in_canvas: bool = False" in document_tool
469 assert '"open_in_canvas": bool(open_in_canvas)' in document_tool
470 assert '"open_in_desktop": bool(open_in_desktop)' in document_tool
471 assert '"requires_desktop": True' in office_api
472 assert 'input.get("open_in_desktop") is not True' in office_api
473 assert 'action == "desktop"' not in office_api
474 assert 'action == "desktop_state"' not in office_api
475 assert 'action == "desktop_shutdown"' not in office_api
476 assert "Text documents use the Editor surface." in office_api
477 assert '"requires_editor": True' not in office_api
478
479
480 def test_editor_plugin_owns_markdown_sessions_and_active_context_extras():
481 editor_plugin = PROJECT_ROOT / "plugins" / "_editor"
482 assert (editor_plugin / "plugin.yaml").exists()
483 assert (editor_plugin / "api" / "editor_session.py").exists()
484 assert (editor_plugin / "api" / "ws_editor.py").exists()
485
486 editor_session = read("plugins", "_editor", "helpers", "markdown_sessions.py")
487 editor_api = read("plugins", "_editor", "api", "editor_session.py")
488 editor_ws = read("plugins", "_editor", "api", "ws_editor.py")
489 editor_context = read("plugins", "_editor", "helpers", "open_files_context.py")
490 office_ws = read("plugins", "_office", "api", "ws_office.py")
491 editor_result_sync = read(
492 "plugins",
493 "_editor",
494 "extensions",
495 "webui",
496 "set_messages_after_loop",
497 "sync-text-editor-results.js",
498 )
499 editor_extras = read(
500 "plugins",
501 "_editor",
502 "extensions",
503 "python",
504 "message_loop_prompts_after",
505 "_55_include_editor_open_files.py",
506 )
507 desktop_context = read(
508 "plugins",
509 "_desktop",
510 "extensions",
511 "python",
512 "message_loop_prompts_after",
513 "_55_include_desktop_state.py",
514 )
515 office_context = read(
516 "plugins",
517 "_office",
518 "extensions",
519 "python",
520 "message_loop_prompts_after",
521 "_55_include_office_canvas_context.py",
522 )
523
524 assert "context_id: str" in editor_session
525 assert "self._active_by_context" in editor_session
526 assert "def list_open" in editor_session
527 assert "session.context_id == context_id" in editor_session
528 assert "dirty" in editor_session
529 assert "active" in editor_session
530 assert 'action == "list"' in editor_api
531 assert 'action == "activate"' in editor_api
532 assert 'event == "editor_activate"' in editor_ws
533 assert "[EDITOR OPEN FILES]" in read("plugins", "_editor", "prompts", "agent.extras.editor_open_files.md")
534 assert "Content is omitted" in editor_context
535 assert "self.agent.context.id" in editor_extras
536 assert "editor_open_files" in editor_extras
537 assert "desktop_state" in desktop_context
538 assert 'pop("office_canvas"' in office_context
539 assert "Office WebSocket editing is not available for text documents; use the Editor surface." in office_ws
540 assert "from plugins._office.helpers import document_store, markdown_sessions" not in office_ws
541 assert not (PROJECT_ROOT / "plugins" / "_office" / "helpers" / "markdown_sessions.py").exists()
542 assert "syncTextEditorResultsIntoOpenEditor" in editor_result_sync
543
544
545 def test_editor_open_file_browser_prefers_context_home_before_workdir_fallback():
546 editor_store = read("plugins", "_editor", "webui", "editor-store.js")
547 start = editor_store.index("async openFileBrowser()")
548 end = editor_store.index("\n async openPath", start)
549 open_file_browser = editor_store[start:end]
550
551 home_lookup = open_file_browser.index('const home = await callEditor("home");')
552 settings_fallback = open_file_browser.index('const response = await callJsonApi("settings_get", null);')
553
554 assert home_lookup < settings_fallback
555 assert "workdirPath = home.path;" in open_file_browser
556 assert "workdirPath = response?.settings?.workdir_path || workdirPath;" in open_file_browser
557 assert "fileBrowserStore.openTextPicker" in open_file_browser
558 assert "selectedFiles" in open_file_browser
559 assert "fileBrowserStore.normalizePath(file.path)" in open_file_browser
560
561
562 def test_editor_toolbar_places_preview_toggle_left_and_save_on_right():
563 editor_panel = read("plugins", "_editor", "webui", "editor-panel.html")
564 editor_store = read("plugins", "_editor", "webui", "editor-store.js")
565 toolbar_start = editor_panel.index('<div class="editor-toolbar"')
566 toolbar_end = editor_panel.index('<div class="editor-search-bar"', toolbar_start)
567 toolbar = editor_panel[toolbar_start:toolbar_end]
568
569 mode_toggle = toolbar.index("editor-mode-toggle")
570 history_tools = toolbar.index("editor-history-tools")
571 source_tools = toolbar.index("editor-source-tools")
572 preview_tools = toolbar.index("editor-preview-tools")
573 spacer = toolbar.index("editor-toolbar-spacer")
574 save_button = toolbar.index("editor-save-button")
575 save_as_button = toolbar.index("editor-save-as-button")
576 file_actions = toolbar.index("editor-file-actions")
577 file_menu = toolbar.index("editor-file-menu")
578
579 assert mode_toggle < history_tools < source_tools
580 assert history_tools < preview_tools
581 assert spacer < save_button < save_as_button < file_actions < file_menu
582 assert "@click=\"$store.editor.save()\"" in toolbar
583 assert "@click=\"$store.editor.saveAs()\"" in toolbar
584 assert "async saveAs()" in editor_store
585 assert "fileBrowserStore.openSaveAsPicker" in editor_store
586 assert 'callEditor("save_as"' in editor_store
587 assert "isTextDocument()" in toolbar
588 assert 'data-editor-new-action="text"' in editor_store
589
590 file_menu_markup = toolbar[file_menu:]
591 assert "<span>Save</span>" not in file_menu_markup
592 assert "<span>Rename</span>" in file_menu_markup
593 assert "<span>Close File</span>" in file_menu_markup
594
595
596 def test_editor_uses_full_document_preview_and_matching_markdown_text_tools():
597 editor_panel = read("plugins", "_editor", "webui", "editor-panel.html")
598 editor_store = read("plugins", "_editor", "webui", "editor-store.js")
599
600 assert "Previous page" not in editor_panel
601 assert "Next page" not in editor_panel
602 assert "editor-page-count" not in editor_panel
603 assert "pagePositionLabel" not in editor_store
604 assert "nextPage()" not in editor_store
605 assert "previousPage()" not in editor_store
606 assert 'x-show="$store.editor.isTextDocument()"' in editor_panel
607 assert '$store.editor.isTextDocument() && $store.editor.isPreviewMode()' in editor_panel
608 assert 'mode === PREVIEW_MODE && this.isTextDocument()' in editor_store
609 assert "if (!this.session || !this.isTextDocument()) return;" in editor_store
610
611 source_tools_start = editor_panel.index('class="editor-tool-group editor-source-tools"')
612 preview_tools_start = editor_panel.index('class="editor-tool-group editor-preview-tools"')
613 source_tools = editor_panel[source_tools_start:preview_tools_start]
614 for action in ("Bold", "Italic", "List", "Numbered list", "Table"):
615 assert f'title="{action}"' in source_tools
616 assert "isMarkdown()" not in source_tools
617
618
619 def test_editor_history_shortcuts_and_toolbar_controls_cover_markdown_and_text():
620 editor_panel = read("plugins", "_editor", "webui", "editor-panel.html")
621 editor_store = read("plugins", "_editor", "webui", "editor-store.js")
622
623 assert '@keydown.capture="$store.editor.handleEditorKeydown($event)"' in editor_panel
624 history_start = editor_panel.index('class="editor-tool-group editor-history-tools"')
625 source_start = editor_panel.index('class="editor-tool-group editor-source-tools"')
626 history_tools = editor_panel[history_start:source_start]
627 assert 'x-show="$store.editor.isTextDocument()"' in history_tools
628 assert 'title="Undo"' in history_tools
629 assert 'title="Redo"' in history_tools
630 assert "$store.editor.previewEditing || !$store.editor.canUndo()" in history_tools
631 assert "$store.editor.previewEditing || !$store.editor.canRedo()" in history_tools
632
633 keydown_start = editor_store.index("handleEditorKeydown(event)")
634 create_start = editor_store.index("\n async create", keydown_start)
635 keydown = editor_store[keydown_start:create_start]
636 assert 'key === "z"' in keydown
637 assert 'key === "y"' in keydown
638 assert "event.stopPropagation()" in keydown
639 assert "this[historyAction]()" in keydown
640 assert "nativeEditing" in keydown
641
642 undo_start = editor_store.index("\n undo()")
643 can_undo_start = editor_store.index("\n canUndo()", undo_start)
644 history_actions = editor_store[undo_start:can_undo_start]
645 assert "sourceEditor.undo" not in history_actions
646 assert "sourceEditor.redo" not in history_actions
647 assert "pushHistory(text, coalesce = false)" in editor_store
648 assert "this.pushHistory(this.editorText, true)" in editor_store
649
650
651 def test_desktop_text_open_with_routes_to_editor_surface():
652 desktop_session = read("plugins", "_desktop", "helpers", "desktop_session.py")
653 desktop_store = read("plugins", "_desktop", "webui", "desktop-store.js")
654 editor_store = read("plugins", "_editor", "webui", "editor-store.js")
655 browser_store = read("plugins", "_browser", "webui", "browser-store.js")
656
657 assert 'EDITOR_HANDLER_DESKTOP_ID = "agent-zero-editor.desktop"' in desktop_session
658 assert "def _write_editor_bridge_script" in desktop_session
659 assert "a0-editor://open?path=" in desktop_session
660 assert "_editor_text_handler_mime_types()" in desktop_session
661 assert '"text/markdown"' in desktop_session
662 assert '"text/x-markdown"' in desktop_session
663 assert '"text/plain"' in desktop_session
664 assert "applications_dir / EDITOR_HANDLER_DESKTOP_ID" in desktop_session
665 assert 'desktop_dir / "Editor.desktop"' in desktop_session
666 assert "Opened text in Editor" in desktop_store
667 assert "registerUrlHandler" in editor_store
668 assert "handleEditorUrlIntent" in editor_store
669 assert 'openLatestSurface("editor"' in editor_store
670
671 # Text files default to LibreOffice Writer inside the Desktop; the Agent
672 # Zero Editor stays available as a secondary "Open With" association.
673 assert 'WRITER_HANDLER_DESKTOP_ID = "libreoffice-writer.desktop"' in desktop_session
674 assert "{mime_type}={WRITER_HANDLER_DESKTOP_ID}" in desktop_session
675 assert "[WRITER_HANDLER_DESKTOP_ID, editor_desktop_id, " in desktop_session
676
677 # The browser surface must not claim a0-editor: intents, otherwise the
678 # editor "Open With" handler lands on an unloadable about:blank page.
679 assert "function isWebUrlIntent" in browser_store
680 assert "if (!isWebUrlIntent(url)) return false;" in browser_store
681
682
683 def test_office_and_desktop_skills_are_rehomed_and_renamed():
684 office_skills = PROJECT_ROOT / "plugins" / "_office" / "skills"
685 desktop_skills = PROJECT_ROOT / "plugins" / "_desktop" / "skills"
686
687 assert not (office_skills / "linux-desktop").exists()
688 assert (desktop_skills / "linux-desktop" / "SKILL.md").exists()
689 assert (office_skills / "office-artifacts" / "SKILL.md").exists()
690 assert not (office_skills / "document-artifacts").exists()
691 assert not (office_skills / "word-documents").exists()
692 assert not (office_skills / "excel-workbooks").exists()
693 assert not (office_skills / "presentation-decks").exists()
694
695 expected = {
696 "office-artifacts": office_skills / "office-artifacts" / "SKILL.md",
697 "writer-documents": office_skills / "writer-documents" / "SKILL.md",
698 "calc-spreadsheets": office_skills / "calc-spreadsheets" / "SKILL.md",
699 "impress-presentations": office_skills / "impress-presentations" / "SKILL.md",
700 "markdown-documents": office_skills / "markdown-documents" / "SKILL.md",
701 }
702 for name, path in expected.items():
703 text = path.read_text(encoding="utf-8")
704 assert f"name: {name}" in text
705
706 desktop_skill = (desktop_skills / "linux-desktop" / "SKILL.md").read_text(encoding="utf-8")
707 desktopctl = (desktop_skills / "linux-desktop" / "scripts" / "desktopctl.sh").read_text(encoding="utf-8")
708 assert "/a0/plugins/_desktop/skills/linux-desktop/scripts/desktopctl.sh" in desktop_skill
709 assert "Open in Desktop action" in desktop_skill
710 assert "$BASE_DIR/usr/plugins/_desktop/profiles/$SESSION" in desktopctl
711 assert "$BASE_DIR/usr/plugins/_desktop/sessions/$SESSION.json" in desktopctl
712 assert "sequence|batch)" in desktopctl
713 assert "dispatch_command $line" in desktopctl
714 assert '"$0" $line' not in desktopctl
715
716
717 def test_skill_catalog_and_connector_boundaries_are_static_guarded():
718 skills_py = read("helpers", "skills.py")
719 connector_list = read("plugins", "_a0_connector", "api", "v1", "skills_list.py")
720 connector_delete = read("plugins", "_a0_connector", "api", "v1", "skills_delete.py")
721
722 assert "RENAMED_SKILLS" not in skills_py
723 assert "RENAMED_SKILL_PATHS" not in skills_py
724 assert "_migrate_skill_name" not in skills_py
725 assert "_migrate_skill_path" not in skills_py
726 assert "Built-in plugin skills cannot be deleted" in skills_py
727 assert "list_skill_catalog" in connector_list
728 assert "list_skills(" not in connector_list
729 assert '"origin": skill["origin"]' in connector_list
730 assert "list_skill_catalog" in connector_delete
731 assert 'match.get("origin") not in {"User", "Project"}' in connector_delete
732 assert "only user or project skills can be deleted" in connector_delete