Add refactor guardrails and runtime docs
Some checks are pending
Build And Publish Docker Images / plan (push) Waiting to run
Build And Publish Docker Images / build (push) Blocked by required conditions

Cover the modal/surface boundary, Desktop ownership, Office document-only behavior, explicit Desktop opens, plugin-owned runtime paths, renamed skills, connector ownership rules, Browser context handoff, and Playwright cache stability. Update operator docs to match the retained Docker Playwright install path.
This commit is contained in:
Alessandro 2026-05-07 00:15:50 +02:00
parent 3368fccb3c
commit d637f3c2b6
7 changed files with 911 additions and 711 deletions

View file

@ -27,7 +27,7 @@ Refer to the [Choosing your LLMs](../setup/installation.md#installing-and-using-
Use **Settings → Backup & Restore** and avoid mapping the entire `/a0` directory. See [How to update Agent Zero](../setup/installation.md#how-to-update-agent-zero).
**8. My browser tool fails or says Playwright is missing. What now?**
The built-in browser is provided by the `_browser` plugin and the direct `browser` tool. **Docker:** the Chromium headless shell is shipped preinstalled (typically under `/a0/tmp/playwright`). **Local development:** if the binary is missing, `ensure_playwright_binary()` in `plugins/_browser/helpers/playwright.py` runs `playwright install chromium --only-shell` into `tmp/playwright` on first browser use (you may see UI notifications). To install ahead of time, run `PLAYWRIGHT_BROWSERS_PATH=tmp/playwright playwright install chromium --only-shell` after `pip install -r requirements.txt`. If you prefer an external browser stack, use MCP alternatives such as Browser OS, Chrome DevTools, or Playwright MCP. See [MCP Setup](mcp-setup.md).
The built-in browser is provided by the `_browser` plugin and the direct `browser` tool. **Docker:** the Chromium headless shell is shipped preinstalled (typically under `/a0/usr/plugins/_browser/playwright`). **Local development:** if the binary is missing, `ensure_playwright_binary()` in `plugins/_browser/helpers/playwright.py` runs `playwright install chromium --only-shell` into `usr/plugins/_browser/playwright` on first browser use (you may see UI notifications). To install ahead of time, run `PLAYWRIGHT_BROWSERS_PATH=usr/plugins/_browser/playwright playwright install chromium --only-shell` after `pip install -r requirements.txt`. If you prefer an external browser stack, use MCP alternatives such as Browser OS, Chrome DevTools, or Playwright MCP. See [MCP Setup](mcp-setup.md).
**9. My secrets disappeared after a backup restore.**
Secrets are stored in `/a0/usr/secrets.env` and are not always included in backup archives. Copy them manually.

View file

@ -67,9 +67,9 @@ Now when you select one of the python files in the project, you should see prope
3. Install dependencies. Run these two commands in the terminal:
```bash
pip install -r requirements.txt
PLAYWRIGHT_BROWSERS_PATH=tmp/playwright playwright install chromium --only-shell
PLAYWRIGHT_BROWSERS_PATH=usr/plugins/_browser/playwright playwright install chromium --only-shell
```
The first command installs Python dependencies. The second installs the Chromium headless shell into `tmp/playwright` ahead of time (same path in Docker: `/a0/tmp/playwright`). If you skip the second command, **local development** still downloads the shell on first browser use through `ensure_playwright_binary()` in `plugins/_browser/helpers/playwright.py`. Pre-installing avoids that wait. **Docker** images ship the shell preinstalled; runtime install is for local dev when the binary is missing.
The first command installs Python dependencies. The second installs the Chromium headless shell into `usr/plugins/_browser/playwright` ahead of time (same path in Docker: `/a0/usr/plugins/_browser/playwright`). If you skip the second command, **local development** still downloads the shell on first browser use through `ensure_playwright_binary()` in `plugins/_browser/helpers/playwright.py`. Pre-installing avoids that wait. **Docker** images ship the shell preinstalled; runtime install is for local dev when the binary is missing.
Errors in the code editor caused by missing packages should now be gone. If not, try reloading the window.

View file

@ -258,6 +258,32 @@ def test_browser_launch_config_uses_full_chromium_for_all_sessions(tmp_path):
def test_browser_playwright_cache_uses_persistent_usr_path(monkeypatch, tmp_path):
monkeypatch.delenv("A0_BROWSER_PLAYWRIGHT_CACHE_DIR", raising=False)
monkeypatch.setattr(
browser_playwright_module.files,
"get_abs_path",
lambda *parts: str(tmp_path.joinpath(*parts)),
)
browser_binary = (
tmp_path
/ "usr"
/ "plugins"
/ "_browser"
/ "playwright"
/ "chromium-1169"
/ "chrome-linux"
/ "chrome"
)
browser_binary.parent.mkdir(parents=True)
browser_binary.write_text("#!/bin/sh\n", encoding="utf-8")
assert get_playwright_cache_dir() == str(
tmp_path / "usr" / "plugins" / "_browser" / "playwright"
)
assert get_playwright_binary() == browser_binary
def test_browser_playwright_cache_falls_back_to_existing_legacy_install(monkeypatch, tmp_path):
monkeypatch.delenv("A0_BROWSER_PLAYWRIGHT_CACHE_DIR", raising=False)
monkeypatch.setattr(
browser_playwright_module.files,
@ -275,9 +301,11 @@ def test_browser_playwright_cache_uses_persistent_usr_path(monkeypatch, tmp_path
legacy_binary.parent.mkdir(parents=True)
legacy_binary.write_text("#!/bin/sh\n", encoding="utf-8")
assert get_playwright_cache_dir() == str(
tmp_path / "usr" / "plugins" / "_browser" / "playwright"
)
assert browser_playwright_module.get_playwright_cache_dirs() == [
tmp_path / "usr" / "plugins" / "_browser" / "playwright",
tmp_path / "usr" / "browser" / "playwright",
tmp_path / "tmp" / "playwright",
]
assert get_playwright_binary() == legacy_binary
@ -288,7 +316,7 @@ def test_browser_extension_storage_uses_plugin_user_path(monkeypatch, tmp_path):
lambda *parts: str(tmp_path.joinpath(*parts)),
)
assert get_extensions_root() == tmp_path / "usr" / "plugins" / "_browser" / "extensions"
assert get_extensions_root() == tmp_path / "usr" / "_browser" / "extensions"
def test_browser_extension_manager_uninstalls_only_managed_extensions(monkeypatch, tmp_path):
@ -472,7 +500,8 @@ def test_browser_viewer_creates_chat_when_no_context_is_selected():
assert "chatsStore.setSelected?.(contextId)" in js
assert "this.contextId = existingContextId;" in js
assert "this.contextId = contextId;" in js
assert "let targetContextId = requestedContextId;" in js
assert "let targetContextId = requestedContextId" in js
assert "|| this.resolveContextId();" in js
assert "targetContextId = await this.ensureContextId();" in js
assert "contextId: targetContextId" in js
assert "No active chat context is selected." not in js
@ -496,6 +525,30 @@ def test_browser_canvas_startup_waits_for_raw_viewport_settle():
assert "isCurrentSurfaceOpen(surfaceSequence)" in js
assert "isCanvasSurfaceVisible(element)" in js
assert "scheduleViewportSyncForSurface" in js
assert "const targetChanged = Boolean(" in js
assert "if (this.frameSrc && !targetChanged)" in js
assert "this.cancelFrameRender();" in js
assert "this.resetRenderedFrame();" in js
def test_browser_surface_handoffs_keep_existing_frame_until_replacement_arrives():
js = (PROJECT_ROOT / "plugins" / "_browser" / "webui" / "browser-store.js").read_text(
encoding="utf-8"
)
prepare_start = js.index("prepareSurfaceOpen(nextMode")
prepare_block = js[prepare_start: js.index("resetViewportTracking()", prepare_start)]
viewport_start = js.index("resetRenderedFrameIfViewportChanged(viewport =")
viewport_block = js[viewport_start: js.index("async waitForSurfaceViewport", viewport_start)]
clear_start = js.index("clearRenderedFrameIfViewportChanged()")
clear_block = js[clear_start: js.index("beginCommand()", clear_start)]
assert "modeChanged" not in prepare_block
assert "if (this.frameSrc && !targetChanged)" in prepare_block
assert "this.resetRenderedFrame();" in prepare_block
assert "this.cancelFrameRender();" in viewport_block
assert "this.resetRenderedFrame();" not in viewport_block
assert "this.cancelFrameRender();" in clear_block
assert "this.resetRenderedFrame();" not in clear_block
def test_browser_canvas_surface_open_waits_for_visible_panel():
@ -595,7 +648,7 @@ def test_browser_entry_points_prefer_canvas_and_modal_dock_handoff():
/ "_browser"
/ "extensions"
/ "webui"
/ "right_canvas_register_surfaces"
/ "surfaces_register"
/ "register-browser.js"
).read_text(encoding="utf-8")
browser_store = (PROJECT_ROOT / "plugins" / "_browser" / "webui" / "browser-store.js").read_text(
@ -605,10 +658,11 @@ def test_browser_entry_points_prefer_canvas_and_modal_dock_handoff():
encoding="utf-8"
)
modals_js = (PROJECT_ROOT / "webui" / "js" / "modals.js").read_text(encoding="utf-8")
surfaces_js = (PROJECT_ROOT / "webui" / "js" / "surfaces.js").read_text(encoding="utf-8")
assert "Open Browser" in button_html
assert "$store.rightCanvas ? $store.rightCanvas.open('browser')" in button_html
assert "window.ensureModalOpen ? window.ensureModalOpen('/plugins/_browser/webui/main.html')" in button_html
assert "import('/js/surfaces.js')" in button_html
assert "open('browser')" in button_html
assert "$store.rightCanvas.toggle('browser')" not in button_html
assert 'defaultOpenMode: "modal"' not in register_js
assert "beginDockHandoff()" in register_js
@ -623,11 +677,9 @@ def test_browser_entry_points_prefer_canvas_and_modal_dock_handoff():
assert "await surface.cancelDockHandoff?.(payload)" in canvas_store
assert "async closeDockSourceModal" in canvas_store
assert "sourceModalPath: modal.path" in modals_js
assert "closeSourceModal: async () =>" in modals_js
assert "const closed = await closeModal(modal.path)" in modals_js
assert "const fallbackClosed = await closeModal()" in modals_js
assert "button.disabled = true" in modals_js
assert "dock(metadata.surfaceId" in surfaces_js
assert "button.disabled = true" in surfaces_js
assert "dockSurface(metadata.surfaceId" not in modals_js
assert "beginSurfaceHandoff()" in browser_store
assert "finishSurfaceHandoff()" in browser_store
@ -635,28 +687,36 @@ def test_browser_entry_points_prefer_canvas_and_modal_dock_handoff():
assert "releaseSurfaceBindings()" in browser_store
assert "this.releaseSurfaceBindings();" in browser_store
assert "async function openBrowserCanvas" in tool_handler
assert 'await rightCanvasStore.open("browser", payload);' in tool_handler
assert "window.ensureModalOpen" in tool_handler
assert "window.openModal" in tool_handler
assert "async function openBrowserSurface" in tool_handler
assert 'await openSurface("browser", payload);' in tool_handler
assert "rightCanvasStore" not in tool_handler
assert "window.ensureModalOpen" not in tool_handler
assert "window.openModal" not in tool_handler
assert "function syncOpenBrowserCanvas" in tool_handler
assert "async function syncOpenBrowserCanvas" in after_loop_handler
assert "syncBrowserResultsIntoOpenCanvas" in after_loop_handler
assert '${contextId || ""}' in tool_handler
assert "contextId || \"\"" in after_loop_handler
assert '"context_id"' in after_loop_handler
assert '"contextId"' in after_loop_handler
assert "rightCanvasStore" not in after_loop_handler
assert "window.ensureModalOpen" not in after_loop_handler
assert "window.openModal" not in after_loop_handler
for js in (tool_handler, after_loop_handler):
assert "openBrowserModal" not in js
assert "isBrowserCanvasAlreadyOpen" in js
assert "rightCanvasStore?.isOpen" in js
assert 'rightCanvasStore?.activeSurfaceId === "browser"' in js
assert '[data-surface-id="browser"].is-active .browser-panel' in js
assert "autoOpenBrowserCanvas" not in js
assert "autoOpenedBrowsers" not in js
assert "syncedBrowserCanvases" in js
assert "const FOCUS_ACTIONS = new Set" in js
assert "FOCUS_ACTIONS.has(action)" in js
for js in (tool_handler, after_loop_handler, register_js, browser_store, modals_js):
assert 'id: "browser"' in surfaces_js
assert "/plugins/_browser/webui/main.html" in surfaces_js
for js in (tool_handler, after_loop_handler, register_js, browser_store, modals_js, surfaces_js):
assert "globalThis.Alpine" not in js
assert "Alpine?.store" not in js
assert "Alpine.store" not in js
@ -671,9 +731,23 @@ def test_browser_and_desktop_surface_buttons_remember_latest_window_mode():
)
modals_js = (PROJECT_ROOT / "webui" / "js" / "modals.js").read_text(encoding="utf-8")
modals_css = (PROJECT_ROOT / "webui" / "css" / "modals.css").read_text(encoding="utf-8")
surface_button_block = modals_js[
modals_js.index("function createModalSurfaceButton"):
modals_js.index("function configureModalSurfaceSwitcher")
surfaces_js = (PROJECT_ROOT / "webui" / "js" / "surfaces.js").read_text(encoding="utf-8")
surfaces_css = (PROJECT_ROOT / "webui" / "css" / "surfaces.css").read_text(encoding="utf-8")
close_block = canvas_store[
canvas_store.index("async close()"):
canvas_store.index("async dockSurface")
]
undock_block = canvas_store[
canvas_store.index("async undockSurface"):
canvas_store.index("async openModalSurface")
]
open_modal_block = canvas_store[
canvas_store.index("async openModalSurface"):
canvas_store.index("async undockActiveSurface")
]
surface_button_block = surfaces_js[
surfaces_js.index("function createModalSurfaceButton"):
surfaces_js.index("function configureModalSurfaceSwitcher")
]
assert "surfaceModes: {}" in canvas_store
@ -685,38 +759,57 @@ def test_browser_and_desktop_surface_buttons_remember_latest_window_mode():
assert "isSurfaceVisible(id)" in canvas_store
assert "async openLatest(surfaceId" in canvas_store
assert "async openModalSurface(surfaceId" in canvas_store
assert "this.recordSurfaceMode(targetId, SURFACE_MODE_CANVAS" in canvas_store
assert "this.recordSurfaceMode(targetId, SURFACE_MODE_MODAL)" in canvas_store
assert "this.recordSurfaceMode(targetId, SURFACE_MODE_DOCKED" in canvas_store
assert "this.recordSurfaceMode(targetId, SURFACE_MODE_FLOATING)" in canvas_store
assert "surfaceModes: this.surfaceModes" in canvas_store
assert "normalizeSurfaceMode(mode)" in canvas_store
assert "migratePersistedSurfaceState" in canvas_store
assert "this.mountedSurfaces = {}" not in close_block
assert "surface?.close" not in close_block
assert "this.mountedSurfaces = {}" not in undock_block
assert "failed to close while undocking" not in undock_block
assert "this.mountedSurfaces = {}" not in open_modal_block
assert "failed to close before modal open" not in open_modal_block
assert '@click="$store.rightCanvas.openLatest(surface.id)"' in canvas_html
assert '@click="$store.rightCanvas.open(surface.id)"' in canvas_html
assert 'rightCanvasStore.recordSurfaceMode?.(metadata.surfaceId, "modal")' in modals_js
assert "configureModalSurfaceSwitcher" in modals_js
assert "modal-surface-switcher" in modals_js
assert "modal-surface-button" in modals_js
assert "SINGLE_VISIBLE_MODAL_SURFACE_PATHS" in modals_js
assert "modal-surface-parked" in modals_js
assert "parkSiblingSurfaceModals(activeModal)" in modals_js
assert "recordMode(metadata.surfaceId, SURFACE_MODE_FLOATING)" in surfaces_js
assert "configureModalSurfaceSwitcher" in surfaces_js
assert "surface-switcher" in surfaces_js
assert "surface-button" in surfaces_js
assert "SINGLE_VISIBLE_MODAL_SURFACE_PATHS" not in modals_js
assert "modal-surface-parked" in surfaces_js
assert "parkSiblingSurfaceModals(activeModal)" in surfaces_js
assert "activateModal(modal)" in modals_js
assert "closeSurfaceGroupModals" not in modals_js
assert "closeSurfaceGroupModals" in surfaces_js
assert "const closed = await closeSurfaceGroupModals()" in surfaces_js
assert "globalThis.closeSurfaceGroupModals = closeSurfaceGroupModals" in surfaces_js
assert "button.title = title" not in modals_js
assert "button.title = metadata.title" not in modals_js
assert "rightCanvasStore.panelSurfaces" in modals_js
assert 'rightCanvasStore.recordSurfaceMode?.(surface.id, "modal")' in modals_js
assert "rightCanvasStore.panelSurfaces" not in modals_js
assert 'await recordMode(normalizedId, SURFACE_MODE_FLOATING)' in surfaces_js
assert "const openPromise = ensureModalOpen(targetModalPath)" in surface_button_block
assert "await closeModal(modal.path)" not in surface_button_block
assert "modalRequiresExplicitClose" in modals_js
assert '"plugins/_browser/webui/main.html"' in modals_js
assert '"plugins/_office/webui/main.html"' in modals_js
assert "modalSurfaceMetadata" not in modals_js
assert "modal-content-loaded" in modals_js
assert '"plugins/_browser/webui/main.html"' not in modals_js
assert '"plugins/_office/webui/main.html"' not in modals_js
assert "&& !modalRequiresExplicitClose(newModal)" in modals_js
assert "if (modalRequiresExplicitClose(modalStack[modalStack.length - 1])) return;" in modals_js
assert ".modal-surface-switcher" in modals_css
assert ".modal-surface-button.is-active" in modals_css
assert ".modal-surface-image" in modals_css
assert ".modal.modal-surface-parked" in modals_css
assert "grid-auto-flow: column" in modals_css
assert ".modal-surface-switcher" not in modals_css
assert ".surface-switcher" in surfaces_css
assert ".surface-button" in surfaces_css
assert ".modal-surface-button.is-active" in surfaces_css
assert ".modal-surface-image" in surfaces_css
assert ".modal.modal-surface-parked" in surfaces_css
assert "grid-auto-flow: column" in surfaces_css
assert 'id: "browser"' in surfaces_js
assert 'id: "desktop"' in surfaces_js
assert "/plugins/_browser/webui/main.html" in surfaces_js
assert "/plugins/_desktop/webui/main.html" in surfaces_js
def test_browser_tool_does_not_auto_open_canvas_policy_is_documented():
@ -730,8 +823,8 @@ def test_browser_tool_does_not_auto_open_canvas_policy_is_documented():
encoding="utf-8"
)
assert "must not open the right canvas automatically" in prompt
assert "Use the tool headlessly unless the user opens the Browser canvas" in prompt
assert "must not open a Browser surface automatically" in prompt
assert "Use the tool headlessly unless the user opens the Browser surface" in prompt
assert "optional visible WebUI viewer" in prompt
assert "screenshot" in prompt
assert "vision_load" in prompt
@ -741,7 +834,7 @@ def test_browser_tool_does_not_auto_open_canvas_policy_is_documented():
assert "browser-forms" in prompt
assert "does not automatically load screenshots" in prompt
assert "already open" in config
assert "already-open Browser canvas" in config_html
assert "already-open Browser surface" in config_html
def test_browser_forms_skill_is_plugin_owned_and_discoverable():
@ -849,6 +942,12 @@ def test_browser_viewer_uses_tabs_for_session_switching():
assert "activeBrowserContextId" in browser_store
assert "sameBrowserTab" in browser_store
assert "applyBrowserListing" in browser_store
assert "syncViewerToSelectedContext(selectedContextId)" in browser_store
assert "async syncViewerToSelectedContext" in browser_store
assert "isVisibleBrowserSurface()" in browser_store
assert "firstBrowserInContext(selectedContextId)" in browser_store
assert "requestedContextId && requestedContextId !== inFlightContextId" in browser_store
assert "create_browser: Boolean(options.createBrowser || options.create_browser)" in browser_store
assert "browserTabTooltip(browser)" in browser_store
assert "browserChatTitle(browser = {})" in browser_store
assert "contextId.slice" not in browser_store
@ -1560,9 +1659,12 @@ async def test_browser_viewer_subscribe_unregisters_stream(monkeypatch):
return {"id": 1, "state": {"id": 1, "currentUrl": "about:blank"}}
raise AssertionError(method)
fake_runtime = FakeRuntime()
async def fake_get_runtime(context_id, create=True):
assert context_id == "ctx"
return FakeRuntime()
assert create is False
return fake_runtime
monkeypatch.setattr(ws_browser_module, "get_runtime", fake_get_runtime)
monkeypatch.setattr(
@ -1584,6 +1686,8 @@ async def test_browser_viewer_subscribe_unregisters_stream(monkeypatch):
)
assert result["context_id"] == "ctx"
assert result["active_browser_id"] is None
assert fake_runtime.opened is False
assert ("sid-1", "ctx") in ws_browser_module.WsBrowser._streams
await handler.on_disconnect("sid-1")
@ -1591,6 +1695,88 @@ async def test_browser_viewer_subscribe_unregisters_stream(monkeypatch):
assert ("sid-1", "ctx") not in ws_browser_module.WsBrowser._streams
@pytest.mark.anyio
async def test_browser_viewer_subscribe_can_create_blank_tab_when_requested(monkeypatch):
class FakeRuntime:
def __init__(self) -> None:
self.opened = False
async def call(self, method, *args):
if method == "list":
if self.opened:
return {
"browsers": [{"id": 1, "currentUrl": "about:blank", "title": ""}],
"last_interacted_browser_id": 1,
}
return {"browsers": [], "last_interacted_browser_id": None}
if method == "open":
self.opened = True
return {"id": 1, "state": {"id": 1, "currentUrl": "about:blank"}}
raise AssertionError(method)
fake_runtime = FakeRuntime()
async def fake_get_runtime(context_id, create=True):
assert context_id == "ctx"
assert create is True
return fake_runtime
monkeypatch.setattr(ws_browser_module, "get_runtime", fake_get_runtime)
monkeypatch.setattr(
ws_browser_module.AgentContext,
"get",
staticmethod(lambda context_id: SimpleNamespace(id=context_id)),
)
handler = ws_browser_module.WsBrowser(
SimpleNamespace(),
threading.RLock(),
manager=None,
)
result = await handler.process(
"browser_viewer_subscribe",
{"context_id": "ctx", "create_browser": True},
"sid-create",
)
assert result["active_browser_id"] == 1
assert fake_runtime.opened is True
await handler.on_disconnect("sid-create")
@pytest.mark.anyio
async def test_browser_viewer_subscribe_without_runtime_does_not_create_runtime(monkeypatch):
async def fake_get_runtime(context_id, create=True):
assert context_id == "ctx"
assert create is False
return None
monkeypatch.setattr(ws_browser_module, "get_runtime", fake_get_runtime)
monkeypatch.setattr(
ws_browser_module.AgentContext,
"get",
staticmethod(lambda context_id: SimpleNamespace(id=context_id)),
)
handler = ws_browser_module.WsBrowser(
SimpleNamespace(),
threading.RLock(),
manager=None,
)
result = await handler.process(
"browser_viewer_subscribe",
{"context_id": "ctx"},
"sid-empty",
)
assert result["active_browser_id"] is None
assert result["browsers"] == []
assert ("sid-empty", "ctx") not in ws_browser_module.WsBrowser._streams
@pytest.mark.anyio
async def test_browser_runtime_sessions_are_context_qualified(monkeypatch):
class FakeRuntime:

View file

@ -6,536 +6,282 @@ from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[1]
def test_document_canvas_uses_markdown_editor_and_official_libreoffice_desktop_frame():
panel = (PROJECT_ROOT / "plugins" / "_office" / "webui" / "office-panel.html").read_text(
encoding="utf-8",
)
store = (PROJECT_ROOT / "plugins" / "_office" / "webui" / "office-store.js").read_text(
encoding="utf-8",
)
canvas_panel = (
PROJECT_ROOT / "plugins" / "_office" / "extensions" / "webui" / "right-canvas-panels" / "office-panel.html"
).read_text(encoding="utf-8")
assert "office-source-editor" in panel
assert "data-office-source" in panel
assert "office-rich-editor" not in panel
assert "office-docx-pages" not in panel
assert "office-desktop-frame" in panel
assert "data-office-desktop-host" in panel
assert 'x-init="$nextTick(() => $store.office.mountDesktopFrameHost($el))"' in panel
assert 'x-effect="$store.office.attachDesktopFrame($el)"' not in panel
assert "data-office-desktop-frame" in store
assert 'title="LibreOffice desktop"' not in panel
assert 'frame.setAttribute("aria-label", "Desktop")' in store
assert "office-command-button" in panel
assert "office-button-label" in panel
assert "grid-template-columns: minmax(0, 1fr) auto auto auto" in panel
assert "flex-wrap: nowrap" in panel
assert ".modal-inner.office-modal .modal-scroll" in panel
assert "office-modal-resizer" in panel
assert "resize: both" not in panel
assert 'frame.setAttribute("tabindex", "0")' in store
assert "format_underlined" not in panel
assert "format_align_center" not in panel
assert "is-native-tile" not in panel
assert "hasOfficialOffice()" in panel
assert 'title="Rename"' in panel
assert "@click=\"$store.office.renameActiveFile()\"" in panel
assert "office_save" in store
assert "desktop_save" in store
assert "openRenameModal" in store
assert 'callOffice("renamed"' in store
assert "performRename" in store
assert "payload.text" in store
assert "handleActiveFileRenamed" in store
assert "--office-zoom" not in panel
assert "zoom: 1" not in store
assert 'callOffice("desktop")' in store
assert "ensureDesktopSession" in store
assert 'await this.onOpen({ source: "modal" });' in store
assert "setDesktopHostVisible" in store
assert "isDesktopHostVisible" in store
assert "clearDesktopViewportSyncTimers" in store
assert "setDesktopHostVisible" in canvas_panel
assert "queueMicrotask" in canvas_panel
assert "isSurfaceRendered('office')" in canvas_panel
assert "isSurfaceVisible('office')" in canvas_panel
assert "canvas.isSurfaceMounted?.(\"office\")" in store
assert "Starting Agent Zero Desktop environment" in store
assert "handleOfficialOfficeClosed" in store
assert "ResizeObserver" in store
assert "_desktopResizeSuspended" in store
assert "_desktopResizePending" in store
assert "_desktopResizePendingKey" in store
assert "_desktopViewportSyncTimers" in store
assert "shouldDeferDesktopResize" in store
assert "right-canvas-resize-start" in store
assert "right-canvas-resize-end" in store
assert "isDesktopSession" in store
assert "desktopFrame" in store
assert "attachDesktopFrame" in store
assert "mountDesktopFrameHost" in store
assert "desktopFrameSrcMatches" in store
assert "moveDesktopFrameToKeepalive" in store
assert "destroyDesktopFrame" in store
assert "office-desktop-keepalive" in store
assert "DESKTOP_SHUTDOWN_STORAGE_KEY" in store
assert 'callOffice("desktop_shutdown"' in store
assert "intentional_shutdown" in store
assert "restartDesktopSession" in store
assert "shouldShowDesktopEmptyState" in store
assert "Restart Desktop" in panel
assert "office-desktop-empty" in panel
assert "unloadDesktopFrames" in store
assert "restoreDesktopFrames" in store
assert "officeDesktopUnloaded" not in store
assert "primeXpraDesktopFrame" in store
assert "normalizeXpraDesktopWindow" in store
assert "installXpraDesktopWheelBridge" in store
assert "installXpraDesktopAgentBridge" in store
assert "agentZeroDesktop" in store
assert 'callOffice("desktop_state"' in store
assert "desktopToClient" in store
assert "clientToDesktop" in store
assert "requestRefresh" in store
assert "_desktopBridgeReady" in store
assert "_desktopKeyboardCaptureState" in store
assert "installXpraDesktopKeyboardBridge" in store
assert "focusDesktopFrame" in store
assert "_desktopFocusInProgress" in store
assert "if (this._desktopFocusInProgress) return" in store
assert "_desktopKeyboardActive" in store
assert "isEditableInputTarget" in store
assert "reloadDesktopFrame" in store
assert 'result?.reload' in store
assert "a0_reload" in store
assert "const DESKTOP_RESIZE_DELAY_MS = 80" in store
assert "requestServerResize: false" in store
assert "requestRefresh: false" in store
assert "_desktopResizeTarget" in store
assert "requestDesktopViewportSync" in store
assert "syncDesktopViewport" in store
assert "options.serverResize !== false" in store
assert "serverResize: true" in store
assert "server_is_desktop = true" in store
assert "server_resize_exact = true" in store
assert "_set_decorated?.(false)" in store
assert "topoffset = 0" in store
assert ".undecorated" in store
assert "a0-xpra-desktop-frame-css" in store
assert "installXpraDesktopFramePatches" in store
assert "installXpraDesktopClientPatches" in store
assert "patchedNoWindowList" in store
assert "patchedAddWindowListItem" in store
assert "patchedScreenResized" in store
assert "__a0AllowScreenResize" in store
assert "_desktopHeartbeatTimer" in store
assert "office-modal-focus-button" in store
assert "focusButton.title" not in store
assert "officialOfficeUrl" in store
assert 'parsed.searchParams.set("offscreen", secureContext ? "true" : "false")' in store
assert 'parsed.searchParams.set("clipboard_poll", secureContext ? "true" : "false")' in store
assert "hasOfficialOffice" in store
assert "isOfficeSocketData" in store
assert "office_command" not in store
assert "office_key" not in store
assert "office_mouse" not in store
assert ".uno:Bold" not in store
assert "nativeTilesToHtml" not in store
assert "editorContainsFocus" in store
assert "_focusAttempts" in store
assert "_nativeEventQueue" not in store
assert "await this.awaitNativeEvents()" not in store
assert "<p><br></p>" not in store
assert "setupTitle()" not in panel
assert "Setup in progress" not in store
assert "office-log" not in panel
assert "New Writer document" in panel
assert "DOCX</span>" not in panel
assert "$store.office.create('document', 'odt')" in panel
assert "$store.office.create('spreadsheet', 'ods')" in panel
assert "$store.office.create('presentation', 'odp')" in panel
def read(*parts: str) -> str:
return (PROJECT_ROOT.joinpath(*parts)).read_text(encoding="utf-8")
def test_desktop_xpra_canvas_scroll_is_forwarded_to_the_remote_session():
store = (PROJECT_ROOT / "plugins" / "_office" / "webui" / "office-store.js").read_text(
encoding="utf-8",
)
def test_modals_are_generic_and_surfaces_own_live_surface_paths():
modals_js = read("webui", "js", "modals.js")
modals_css = read("webui", "css", "modals.css")
surfaces_js = read("webui", "js", "surfaces.js")
surfaces_css = read("webui", "css", "surfaces.css")
assert "canvas.addEventListener(\"wheel\"" in store
assert "mouse_scroll_cb(normalizedEvent, xpraWindow)" in store
assert "stopImmediatePropagation" in store
assert "{ passive: false, capture: true }" in store
assert "xpraDesktopWheelEvent" in store
assert "deltaMode: { value: 0 }" in store
assert "wheelDeltaY" in store
assert "getModifierState: { value: getModifierState }" in store
def test_office_surface_filters_tabs_to_desktop_and_markdown_without_dashboard():
panel = (PROJECT_ROOT / "plugins" / "_office" / "webui" / "office-panel.html").read_text(
encoding="utf-8",
)
store = (PROJECT_ROOT / "plugins" / "_office" / "webui" / "office-store.js").read_text(
encoding="utf-8",
)
assert "office-card-grid" not in panel
assert "office-document-card" not in panel
assert "visibleTabs()" in panel
assert "openCards()" not in panel
assert "recentCards()" not in panel
assert "office-editor-head" not in panel
assert "office-recent-row" not in panel
assert "open_documents" not in store
assert "installDesktopDocumentSession" in store
assert "isDesktopOfficeDocument" in store
assert "isVisibleOfficeTab" in store
assert "return this.tabs.filter((tab) => this.isVisibleOfficeTab(tab));" in store
file_browser_store = (
PROJECT_ROOT / "webui" / "components" / "modals" / "file-browser" / "file-browser-store.js"
).read_text(encoding="utf-8")
assert "renameAfterConfirm" in file_browser_store
assert "renamePerformAction" in file_browser_store
assert "renameValidateName" in file_browser_store
assert "options.onRenamed" in file_browser_store
assert "options.performRename" in file_browser_store
assert "options.validateName" in file_browser_store
def test_right_canvas_surface_is_branded_as_desktop():
surface = (
PROJECT_ROOT
/ "plugins"
/ "_office"
/ "extensions"
/ "webui"
/ "right_canvas_register_surfaces"
/ "register-office.js"
).read_text(encoding="utf-8")
handler = (
PROJECT_ROOT
/ "plugins"
/ "_office"
/ "extensions"
/ "webui"
/ "get_tool_message_handler"
/ "document-artifact-handler.js"
).read_text(encoding="utf-8")
document_actions = (
PROJECT_ROOT
/ "plugins"
/ "_office"
/ "extensions"
/ "webui"
/ "lib"
/ "document-actions.js"
).read_text(encoding="utf-8")
assert 'title: "Desktop"' in surface
assert 'icon: "desktop_windows"' in surface
assert "buildDocumentFileActionButtons(document)" in handler
assert "Open in canvas" in document_actions
assert "downloadDocument" in document_actions
assert "/api/download_work_dir_file?path=" in document_actions
assert "source: \"message-action\"" in document_actions
def test_official_libreoffice_desktop_route_and_packages_are_declared():
routes = (PROJECT_ROOT / "helpers" / "virtual_desktop_routes.py").read_text(encoding="utf-8")
primitive = (PROJECT_ROOT / "helpers" / "virtual_desktop.py").read_text(encoding="utf-8")
desktop = (
PROJECT_ROOT / "plugins" / "_office" / "helpers" / "libreoffice_desktop.py"
).read_text(encoding="utf-8")
install = (PROJECT_ROOT / "docker" / "run" / "fs" / "ins" / "install_additional.sh").read_text(
encoding="utf-8",
)
linux_desktop_skill = (
PROJECT_ROOT / "plugins" / "_office" / "skills" / "linux-desktop" / "SKILL.md"
).read_text(encoding="utf-8")
linux_desktopctl = (
PROJECT_ROOT / "plugins" / "_office" / "skills" / "linux-desktop" / "scripts" / "desktopctl.sh"
).read_text(encoding="utf-8")
desktop_state_helper = (
PROJECT_ROOT / "plugins" / "_office" / "helpers" / "desktop_state.py"
).read_text(encoding="utf-8")
hooks_py = (PROJECT_ROOT / "plugins" / "_office" / "hooks.py").read_text(encoding="utf-8")
linux_calc_helper = (
PROJECT_ROOT / "plugins" / "_office" / "skills" / "linux-desktop" / "scripts" / "calc_set_cell.py"
).read_text(encoding="utf-8")
assert 'Mount("/desktop"' in routes
assert 'Mount("/libreoffice"' not in routes
assert "http.client.HTTPConnection" in routes
assert "WSConnection" in routes
assert "/session/" in routes
assert "resize_session" in routes
assert "resize_display" in primitive
assert "DEFAULT_HEIGHT = 900" in primitive
assert "MAX_WIDTH = 1920" in primitive
assert "MAX_HEIGHT = 1080" in primitive
assert "xrandr" in primitive
assert "xpra-x11" in primitive
assert "xpramenu" in primitive
assert "floating_menu" in primitive
assert '"file_transfer": "true"' in primitive
assert '"sound": "false"' in primitive
assert '"encoding": "jpeg"' in primitive
assert '"quality": "85"' in primitive
assert '"speed": "80"' in primitive
assert '"printing": "true"' in primitive
assert '"offscreen": "true"' in primitive
assert "xpra" in desktop
assert "xpra-html5" in desktop
assert "Xvfb" in desktop
assert "xfce4-session" in desktop
assert "DISPLAY_START_TIMEOUT_SECONDS" in desktop
assert '"shadow"' in desktop
assert "--resize-display=yes" in desktop
assert "--tray=no" in desktop
assert "--system-tray=no" in desktop
assert "--file-transfer=yes" in desktop
assert "--open-files=no" in desktop
assert "--open-url=no" in desktop
assert "--printing=yes" in desktop
assert "--cursors=no" not in desktop
assert "--audio=no" in desktop
assert "--speaker=off" in desktop
assert "--microphone=off" in desktop
assert "--encoding=jpeg" in desktop
assert "--quality=85" in desktop
assert "--speed=80" in desktop
assert "_restart_xpra_shadow(session)" not in desktop
assert 'result["reload"] = True' not in desktop
assert "MAX_SCREEN_WIDTH}x{MAX_SCREEN_HEIGHT}x24" in desktop
assert '"-ac"' in desktop
assert "SYSTEM_TITLE = \"Desktop\"" in desktop
assert "title=\"Desktop\"" in desktop
assert "--log-file=xpra.log" in desktop
assert "virtual_desktop.session_url" in desktop
assert "xsetroot" in desktop
assert "BLOCKING_DIALOG_TITLES" in desktop
assert "xfce4-terminal" in desktop
assert "thunar" in desktop
assert "Browser.desktop" in desktop
assert "Files.desktop" in desktop
assert "org.xfce.terminal" in desktop
assert "org.xfce.settings.manager" in desktop
assert "firefox-esr" not in desktop
assert "xfce4-settings-manager" in desktop
assert "metadata::xfce-exe-checksum" in desktop
assert "DESKTOP_FOLDER_LINKS" in desktop
assert "HIDDEN_XPRA_DESKTOP_ENTRIES" in desktop
assert "HIDDEN_XFCE_MENU_ENTRIES" in desktop
assert "SHUTDOWN_HANDLER_DESKTOP_ID" in desktop
assert "SHUTDOWN_PANEL_LAUNCHER_ID" in desktop
assert "SHUTDOWN_CONFIRM_SECONDS" in desktop
assert "Shutdown Desktop" in desktop
assert "shutdown-request.json" in desktop
assert "shutdown-request.arm.json" in desktop
assert "shutdown_system_desktop" in desktop
assert "claim_shutdown_request" in desktop
assert "last-show-hidden" in desktop
assert "exo-mail-reader.desktop" in desktop
assert "exo-web-browser.desktop" in desktop
assert "xfce4-mail-reader.desktop" in desktop
assert "xfce4-web-browser.desktop" in desktop
assert "xfce4-session-logout.desktop" in desktop
assert "agent-zero-shutdown.desktop" in desktop
assert "libreoffice-gtk3" in install
assert "libreofficekit" not in install
assert "gir1.2-lokdocview" not in install
assert "python3-gi" not in install
assert "xpra" in install
assert "xpra-x11" in install
assert "xpra-html5" in install
assert "xfce4-session" in install
assert "thunar" in install
assert "libglib2.0-bin" in install
assert "xfce4-terminal" in install
assert "firefox-esr" not in install
assert "pulseaudio" not in install
assert "x11-xserver-utils" in install
assert "xauth" in install
assert "Linux Desktop Interface" in linux_desktop_skill
assert "Use the external Agent Zero Browser" in linux_desktop_skill
assert "/a0/usr/workdir" in linux_desktop_skill
assert "/a0/usr/projects" in linux_desktop_skill
assert "desktopctl.sh" in linux_desktop_skill
assert "/a0/plugins/_office/skills/linux-desktop/scripts/desktopctl.sh" in linux_desktop_skill
assert "calc-set-cell" in linux_desktop_skill
assert "Clicks are explicitly last resort" in linux_desktop_skill or "clicks are explicitly last resort" in linux_desktop_skill
assert "fresh Desktop observation" in linux_desktop_skill
assert "observe --json --screenshot" in linux_desktop_skill
assert "Terminal And CLI Agent Verification" in linux_desktop_skill
assert "Do not report from an earlier screenshot path" in linux_desktop_skill
assert "screenshot path returned by that final observation" in linux_desktop_skill
assert "Never paste natural-language text into that shell prompt" in linux_desktop_skill
assert "command not found" in linux_desktop_skill
assert "TARGET_CLI=\"example-cli-agent\"" in linux_desktop_skill
assert "FALLBACK_CMD" in linux_desktop_skill
assert "@openai/codex" not in linux_desktop_skill
assert "xdotool" in linux_desktopctl
assert "agent-zero-desktop" in linux_desktopctl
assert "launch_app" in linux_desktopctl
assert "paste_key_for_active_window" in linux_desktopctl
assert "active_window_is_terminal" in linux_desktopctl
assert "WM_CLASS" in linux_desktopctl
for command in (
"state)",
"observe)",
"screenshot)",
"active-window)",
"geometry)",
"wait-window)",
"scroll)",
"drag)",
"right-click)",
"paste-text)",
"sequence)",
for forbidden in (
"right-canvas-store",
"/plugins/_browser",
"/plugins/_office",
"/plugins/_desktop",
"SINGLE_VISIBLE_MODAL_SURFACE_PATHS",
"data-canvas",
"surface-window",
):
assert command in linux_desktopctl
assert "calc_set_cell.py" in linux_desktopctl
assert "collect_state" in desktop_state_helper
assert "compact_prompt_context" in desktop_state_helper
assert "fresh final" in desktop_state_helper
assert "xwd" in desktop_state_helper
assert "PIL" in desktop_state_helper
assert '"x11-utils"' in hooks_py
assert '"x11-apps"' in hooks_py
assert '"xclip"' in hooks_py
assert '"python3-pil"' in hooks_py
assert "wait_for_document" in linux_calc_helper
assert "document.store()" in linux_calc_helper
assert "read_xlsx_cell" in linux_calc_helper
assert "DisposedException" in linux_calc_helper
assert forbidden not in modals_js
assert "modalStack" in modals_js
assert 'const backdrop = document.createElement("div")' in modals_js
assert "backdrop.style.display" in modals_js
assert "modalSurfaceMetadata" not in modals_js
assert "modal-content-loaded" in modals_js
assert ".surface-floating" not in modals_css
assert ".surface-switcher" not in modals_css
assert "CORE_SURFACES" in surfaces_js
assert "modalSurfaceMetadata" in surfaces_js
assert "closeSurfaceGroupModals" in surfaces_js
assert 'id: "browser"' in surfaces_js
assert 'id: "desktop"' in surfaces_js
assert "/plugins/_browser/webui/main.html" in surfaces_js
assert "/plugins/_desktop/webui/main.html" in surfaces_js
assert "LEGACY_SURFACE_IDS" in surfaces_js
assert '["office", "desktop"]' in surfaces_js
assert "htmlDataset.surfaceId" in surfaces_js
assert "htmlDataset.canvasSurface" in surfaces_js
assert ".surface-modal" in surfaces_css
assert ".surface-floating" in surfaces_css
assert ".surface-resize-handle" in surfaces_css
assert ".surface-switcher" in surfaces_css
assert "surface-window" not in surfaces_js + surfaces_css
def test_right_canvas_requires_explicit_open_and_is_absent_on_mobile():
canvas_store = (
PROJECT_ROOT / "webui" / "components" / "canvas" / "right-canvas-store.js"
).read_text(encoding="utf-8")
canvas_html = (
PROJECT_ROOT / "webui" / "components" / "canvas" / "right-canvas.html"
).read_text(encoding="utf-8")
canvas_css = (
PROJECT_ROOT / "webui" / "components" / "canvas" / "right-canvas.css"
).read_text(encoding="utf-8")
handler = (
PROJECT_ROOT
/ "plugins"
/ "_office"
/ "extensions"
/ "webui"
/ "get_tool_message_handler"
/ "document-artifact-handler.js"
).read_text(encoding="utf-8")
after_loop = (
PROJECT_ROOT
/ "plugins"
/ "_office"
/ "extensions"
/ "webui"
/ "set_messages_after_loop"
/ "auto-open-document-results.js"
).read_text(encoding="utf-8")
def test_right_canvas_uses_desktop_surface_id_and_migrates_legacy_office_state():
canvas_store = read("webui", "components", "canvas", "right-canvas-store.js")
desktop_register = read(
"plugins",
"_desktop",
"extensions",
"webui",
"surfaces_register",
"register-desktop.js",
)
desktop_panel = read(
"plugins",
"_desktop",
"extensions",
"webui",
"right-canvas-panels",
"desktop-panel.html",
)
desktop_new_menu = read(
"plugins",
"_desktop",
"extensions",
"webui",
"right-canvas-toolbar-start",
"desktop-new-menu.html",
)
right_canvas_css = read("webui", "components", "canvas", "right-canvas.css")
desktop_web_panel = read("plugins", "_desktop", "webui", "desktop-panel.html")
init_registration = canvas_store.index('await callJsExtensions("right_canvas_register_surfaces", this);')
init_ensure = canvas_store.index("this.ensureActiveSurface();", init_registration)
register_surface = canvas_store.index("registerSurface(surface)")
register_guard = canvas_store.index("if (!this._registering)", register_surface)
guarded_ensure = canvas_store.index("this.ensureActiveSurface();", register_guard)
open_surface = canvas_store.index("async open", register_surface)
assert init_registration < init_ensure
assert register_surface < register_guard < guarded_ensure < open_surface
assert "right-canvas-resize-start" in canvas_store
assert "right-canvas-resize-end" in canvas_store
assert "dispatchResizeEvent" in canvas_store
assert "this.isOpen = false;" in canvas_store
assert "wasMobileMode && this.width < MIN_WIDTH" in canvas_store
assert "const MIN_WIDTH = 0" in canvas_store
assert "const MAX_WIDTH" not in canvas_store
assert "0.58" not in canvas_store
assert "min(900px, 58vw)" not in canvas_css
assert "max-width: none" in canvas_css
assert "if (this.isMobileMode && !surface.actionOnly)" in canvas_store
assert "if (this.isMobileMode)" in canvas_store
assert "shouldRender()" in canvas_store
assert "$store.rightCanvas.shouldRender()" in canvas_html
assert 'title="Open as window"' in canvas_html
assert 'title="Close canvas"' in canvas_html
assert 'aria-label="Close canvas"' in canvas_html
assert "@click=\"$store.rightCanvas.close()\"" in canvas_html
assert canvas_html.index('title="Open as window"') < canvas_html.index('title="Close canvas"')
assert "body.right-canvas-mobile-mode .right-canvas" in canvas_css
assert "display: none !important" in canvas_css
assert "autoOpenOfficeCanvas" not in handler
assert "isOfficeCanvasAlreadyOpen" in after_loop
assert 'canvas?.isOpen && canvas?.activeSurfaceId === "office"' in after_loop
assert "office.openSession?.(" in after_loop
assert 'source: "tool-result-sync"' in after_loop
assert 'rightCanvas.open' not in after_loop
assert 'await callJsExtensions("surfaces_register", this);' in canvas_store
assert 'await callJsExtensions("right_canvas_register_surfaces", this);' in canvas_store
assert "migratePersistedSurfaceState" in canvas_store
assert "normalizeSurfaceId" in canvas_store
assert "const saved = migratePersistedSurfaceState(JSON.parse" in canvas_store
assert 'id: "desktop"' in desktop_register
assert 'modalPath: "/plugins/_desktop/webui/main.html"' in desktop_register
assert 'data-surface-id="desktop"' in desktop_panel
assert "isSurfaceVisible('desktop')" in desktop_panel
assert "right-canvas-desktop-actions" in desktop_new_menu
assert "isSurfaceActive('desktop')" in desktop_new_menu
assert "runNewMenuAction('writer')" in desktop_new_menu
assert "runNewMenuAction('spreadsheet')" in desktop_new_menu
assert "runNewMenuAction('presentation')" in desktop_new_menu
assert ".right-canvas-header" in right_canvas_css
assert "overflow: visible;" in right_canvas_css
assert ".right-canvas-toolbar" in right_canvas_css
assert ".right-canvas-desktop-actions .office-new-menu" in desktop_web_panel
assert "z-index: 4000;" in desktop_web_panel
assert not (PROJECT_ROOT / "plugins" / "_office" / "extensions" / "webui" / "right_canvas_register_surfaces" / "register-office.js").exists()
assert not (PROJECT_ROOT / "plugins" / "_office" / "extensions" / "webui" / "right-canvas-panels" / "office-panel.html").exists()
def test_office_skills_preserve_markdown_first_and_opt_in_desktop_policy():
office_skill = (
PROJECT_ROOT / "plugins" / "_office" / "skills" / "office-artifacts" / "SKILL.md"
).read_text(encoding="utf-8")
desktop_skill = (
PROJECT_ROOT / "plugins" / "_office" / "skills" / "linux-desktop" / "SKILL.md"
).read_text(encoding="utf-8")
markdown_skill = (
PROJECT_ROOT / "plugins" / "_office" / "skills" / "markdown-documents" / "SKILL.md"
).read_text(encoding="utf-8")
word_skill = (
PROJECT_ROOT / "plugins" / "_office" / "skills" / "word-documents" / "SKILL.md"
).read_text(encoding="utf-8")
excel_skill = (
PROJECT_ROOT / "plugins" / "_office" / "skills" / "excel-workbooks" / "SKILL.md"
).read_text(encoding="utf-8")
presentation_skill = (
PROJECT_ROOT / "plugins" / "_office" / "skills" / "presentation-decks" / "SKILL.md"
).read_text(encoding="utf-8")
def test_browser_surface_restores_focus_mode_chrome():
browser_store = read("plugins", "_browser", "webui", "browser-store.js")
browser_panel = read("plugins", "_browser", "webui", "browser-panel.html")
assert "ODF is first-class" in office_skill
assert "DOCX, XLSX, or PPTX only" in office_skill
assert "custom document canvas" in office_skill
assert "must not open the canvas automatically" in office_skill
assert "Download and Open in canvas actions" in office_skill
assert "method: \"create\"" in office_skill
assert "The Desktop is opt-in" in desktop_skill
assert "coordinate clicks only as a last resort" in desktop_skill
assert "After any GUI action, verify" in desktop_skill
assert "custom Markdown editor" in desktop_skill
assert "Never open the Desktop/canvas automatically" in desktop_skill
assert "persistent Desktop runtime during initial startup" in desktop_skill
assert '"format": "md"' in markdown_skill
assert "never open the canvas automatically" in markdown_skill
assert '"format": "odt"' in word_skill
assert "DOCX only" in word_skill
assert "must not open the canvas automatically" in word_skill
assert '"format": "ods"' in excel_skill
assert "For a blank workbook request" in excel_skill
assert "must not open the canvas automatically" in excel_skill
assert '"format": "odp"' in presentation_skill
assert "must not open the canvas automatically" in presentation_skill
assert "browser-modal-focus-button" in browser_store
assert "is-focus-mode" in browser_store
assert "fullscreen_exit" in browser_store
assert "Focus mode" in browser_store
assert "Restore size" in browser_store
assert ".modal-inner.browser-modal.is-focus-mode" in browser_panel
def test_office_extra_prompt_includes_existing_desktop_state_without_opening_canvas():
canvas_context = (
PROJECT_ROOT / "plugins" / "_office" / "helpers" / "canvas_context.py"
).read_text(encoding="utf-8")
prompt = (
PROJECT_ROOT / "plugins" / "_office" / "prompts" / "agent.extras.office_canvas.md"
).read_text(encoding="utf-8")
def test_office_frontend_is_document_only_and_does_not_import_browser_or_desktop_runtime_code():
office_store = read("plugins", "_office", "webui", "office-store.js")
office_panel = read("plugins", "_office", "webui", "office-panel.html")
office_modal = read("plugins", "_office", "webui", "main.html")
assert "build_desktop_context" in canvas_context
assert "session_manifest_exists" in canvas_context
assert "collect_state(include_screenshot=False)" in canvas_context
assert "compact_prompt_context" in canvas_context
assert "ensure_system_desktop" not in canvas_context
assert "[DOCUMENT CANVAS]" in prompt
assert "/plugins/_browser" not in office_store
assert "right-canvas-store" not in office_store
assert "handleUrlIntent" not in office_store
assert "ensureDesktopSession" not in office_store
assert "desktop_save" not in office_store
assert "desktop_sync" not in office_store
assert "desktop_state" not in office_store
assert "desktop_shutdown" not in office_store
assert "Xpra" not in office_store
assert "xpra" not in office_store
assert "data-office-desktop-host" not in office_panel
assert "office-desktop-frame" not in office_panel
assert "Restart Desktop" not in office_panel
assert "data-surface-id" not in office_modal
assert "modal-no-backdrop" not in office_modal
assert "data-canvas-surface" not in office_modal
assert "office-source-editor" in office_panel
assert "data-office-source" in office_panel
assert "openRenameModal" in office_store
assert "office_save" in office_store
assert 'callOffice("renamed"' in office_store
assert "requires_desktop" in office_store
assert "openSurface(\"desktop\"" in office_store
def test_desktop_plugin_owns_routes_runtime_surface_and_state_paths():
desktop_plugin = PROJECT_ROOT / "plugins" / "_desktop"
assert (desktop_plugin / "plugin.yaml").exists()
assert (desktop_plugin / "api" / "desktop_session.py").exists()
assert (desktop_plugin / "helpers" / "desktop_session.py").exists()
assert (desktop_plugin / "helpers" / "desktop_state.py").exists()
assert (desktop_plugin / "skills" / "linux-desktop" / "scripts" / "desktopctl.sh").exists()
desktop_startup = read("plugins", "_desktop", "extensions", "python", "startup_migration", "_20_desktop_routes.py")
desktop_api = read("plugins", "_desktop", "api", "desktop_session.py")
desktop_session = read("plugins", "_desktop", "helpers", "desktop_session.py")
desktop_state = read("plugins", "_desktop", "helpers", "desktop_state.py")
desktop_store = read("plugins", "_desktop", "webui", "desktop-store.js")
desktop_main = read("plugins", "_desktop", "webui", "main.html")
desktop_web_panel = read("plugins", "_desktop", "webui", "desktop-panel.html")
assert "virtual_desktop_routes.install_route_hooks()" in desktop_startup
assert 'action in {"open_document", "document"}' in desktop_api
assert 'callJsonApi("/plugins/_desktop/desktop_session"' in desktop_store
assert 'callDesktop("open_document"' in desktop_store
assert 'callOffice("create"' in desktop_store
assert "open_in_desktop: isOfficialExtension(fmt)" in desktop_store
assert "__a0XpraOffsetWarnPatched" in desktop_store
assert "window does not fit in canvas, offsets" in desktop_store
assert "decode error packet" in desktop_store
assert 'data-surface-id="desktop"' in desktop_main
assert "virtual_desktop.session_url" in desktop_session
assert 'owner="desktop"' in desktop_session
assert 'STATE_DIR = Path(files.get_abs_path("usr", "_desktop"))' in desktop_session
assert 'STATE_DIR = BASE_DIR / "usr" / "_desktop"' in desktop_state
assert "> x-component > div[x-data] > .office-panel" in desktop_web_panel
assert ".office-state-line > span:not(.material-symbols-outlined)" in desktop_web_panel
assert not (PROJECT_ROOT / "plugins" / "_office" / "helpers" / "desktop_state.py").exists()
assert not (PROJECT_ROOT / "plugins" / "_office" / "helpers" / "libreoffice_desktop_routes.py").exists()
assert not (PROJECT_ROOT / "plugins" / "_office" / "assets" / "desktop").exists()
def test_plugin_owned_runtime_state_paths_are_declared():
office_documents = read("plugins", "_office", "helpers", "document_store.py")
browser_playwright = read("plugins", "_browser", "helpers", "playwright.py")
browser_extensions = read("plugins", "_browser", "helpers", "extension_manager.py")
docker_playwright = read("docker", "run", "fs", "ins", "install_playwright.sh")
assert 'PLUGIN_NAME = "_office"' in office_documents
assert 'STATE_DIR = Path(files.get_abs_path("usr", PLUGIN_NAME, "documents"))' in office_documents
assert 'PLAYWRIGHT_CACHE_DIR = ("usr", "plugins", "_browser", "playwright")' in browser_playwright
assert "Path(files.get_abs_path(*PLAYWRIGHT_CACHE_DIR))" in browser_playwright
assert "Path(files.get_abs_path(*EXTENSIONS_ROOT_DIR))" in browser_extensions
assert "PLAYWRIGHT_BROWSERS_PATH=/a0/usr/plugins/_browser/playwright" in docker_playwright
def test_document_artifacts_only_open_desktop_from_explicit_document_ui_requests():
auto_open = read(
"plugins",
"_office",
"extensions",
"webui",
"set_messages_after_loop",
"auto-open-document-results.js",
)
document_actions = read("plugins", "_office", "extensions", "webui", "lib", "document-actions.js")
document_tool = read("plugins", "_office", "tools", "document_artifact.py")
office_api = read("plugins", "_office", "api", "office_session.py")
assert 'openSurface("desktop"' in auto_open
assert "isExplicitDocumentUiRequest(payload)" in auto_open
assert 'action === "open"' in auto_open
assert "open_in_canvas" in auto_open
assert "open_in_desktop" in auto_open
assert 'surfaces.open("desktop"' not in auto_open
assert "rightCanvas.open" not in auto_open
assert "globalThis.Alpine" not in auto_open
assert "syncDocumentResultsIntoOpenOfficeModal" in auto_open
assert "isOfficeCanvas" not in auto_open
assert "officeStore" in auto_open
assert "openDocumentInDesktop" in document_actions
assert "openDocumentArtifact" in document_actions
assert "ensureModalOpen" in document_actions
assert "Open Document" in document_actions
assert 'openSurface("desktop"' in document_actions
assert "Edit in Writer" in document_actions
assert "Edit in Calc" in document_actions
assert "Edit in Impress" in document_actions
assert "open_in_canvas: bool = False" in document_tool
assert '"open_in_canvas": bool(open_in_canvas)' in document_tool
assert '"open_in_desktop": bool(open_in_desktop)' in document_tool
assert '"requires_desktop": True' in office_api
assert 'input.get("open_in_desktop") is not True' in office_api
def test_office_and_desktop_skills_are_rehomed_and_renamed():
office_skills = PROJECT_ROOT / "plugins" / "_office" / "skills"
desktop_skills = PROJECT_ROOT / "plugins" / "_desktop" / "skills"
assert not (office_skills / "linux-desktop").exists()
assert (desktop_skills / "linux-desktop" / "SKILL.md").exists()
assert not (office_skills / "office-artifacts").exists()
assert not (office_skills / "word-documents").exists()
assert not (office_skills / "excel-workbooks").exists()
assert not (office_skills / "presentation-decks").exists()
expected = {
"document-artifacts": office_skills / "document-artifacts" / "SKILL.md",
"writer-documents": office_skills / "writer-documents" / "SKILL.md",
"calc-spreadsheets": office_skills / "calc-spreadsheets" / "SKILL.md",
"impress-presentations": office_skills / "impress-presentations" / "SKILL.md",
"markdown-documents": office_skills / "markdown-documents" / "SKILL.md",
}
for name, path in expected.items():
text = path.read_text(encoding="utf-8")
assert f"name: {name}" in text
desktop_skill = (desktop_skills / "linux-desktop" / "SKILL.md").read_text(encoding="utf-8")
desktopctl = (desktop_skills / "linux-desktop" / "scripts" / "desktopctl.sh").read_text(encoding="utf-8")
assert "/a0/plugins/_desktop/skills/linux-desktop/scripts/desktopctl.sh" in desktop_skill
assert "Open in Desktop action" in desktop_skill
assert "$BASE_DIR/usr/_desktop/profiles/$SESSION" in desktopctl
assert "$BASE_DIR/usr/_desktop/sessions/$SESSION.json" in desktopctl
def test_skill_catalog_and_connector_boundaries_are_static_guarded():
skills_py = read("helpers", "skills.py")
connector_list = read("plugins", "_a0_connector", "api", "v1", "skills_list.py")
connector_delete = read("plugins", "_a0_connector", "api", "v1", "skills_delete.py")
assert "RENAMED_SKILLS" not in skills_py
assert "RENAMED_SKILL_PATHS" not in skills_py
assert "_migrate_skill_name" not in skills_py
assert "_migrate_skill_path" not in skills_py
assert "Built-in plugin skills cannot be deleted" in skills_py
assert "list_skill_catalog" in connector_list
assert "list_skills(" not in connector_list
assert '"origin": skill["origin"]' in connector_list
assert "list_skill_catalog" in connector_delete
assert 'match.get("origin") not in {"User", "Project"}' in connector_delete
assert "only user or project skills can be deleted" in connector_delete

View file

@ -10,7 +10,7 @@ PROJECT_ROOT = Path(__file__).resolve().parents[1]
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from plugins._office.helpers import desktop_state
from plugins._desktop.helpers import desktop_state
def _completed(command, returncode=0, stdout="", stderr=""):

View file

@ -19,12 +19,13 @@ if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from plugins._office import hooks
from plugins._desktop import hooks as desktop_hooks
from plugins._desktop.helpers import desktop_session
from plugins._office.helpers import (
artifact_editor,
canvas_context,
document_store,
libreoffice,
libreoffice_desktop,
markdown_sessions,
)
@ -312,8 +313,8 @@ def test_odf_is_advertised_and_docx_remains_explicit_compatibility(office_state)
assert "ODF is first-class for LibreOffice" in prompt
assert "DOCX/XLSX/PPTX are compatibility formats" in prompt
assert "`method` is accepted as an alias for action" in prompt
assert "they do not open the canvas automatically" in prompt
assert "Download and Open in canvas message actions" in prompt
assert "they do not open a surface automatically" in prompt
assert "explicit Download, Open Document, or Desktop edit message actions" in prompt
doc = document_store.create_document("document", "Use ODT", "odt", "")
assert doc["extension"] == "odt"
@ -450,7 +451,7 @@ def test_thunar_defaults_preserve_existing_profile_settings(tmp_path):
encoding="utf-8",
)
libreoffice_desktop._write_thunar_defaults(thunar_xml)
desktop_session._write_thunar_defaults(thunar_xml)
root = ET.parse(thunar_xml).getroot()
values = {child.get("name"): child.get("value") for child in root.findall("property")}
@ -459,14 +460,14 @@ def test_thunar_defaults_preserve_existing_profile_settings(tmp_path):
assert values["last-show-hidden"] == "true"
def test_official_libreoffice_desktop_status_and_url_contract(tmp_path, monkeypatch):
def test_official_desktop_session_status_and_url_contract(tmp_path, monkeypatch):
xpra_html = tmp_path / "xpra" / "www"
xpra_html.mkdir(parents=True)
(xpra_html / "index.html").write_text("xpra", encoding="utf-8")
monkeypatch.setattr(libreoffice_desktop.libreoffice, "find_soffice", lambda: "/usr/bin/soffice")
monkeypatch.setattr(desktop_session.libreoffice, "find_soffice", lambda: "/usr/bin/soffice")
monkeypatch.setattr(
libreoffice_desktop.shutil,
desktop_session.shutil,
"which",
lambda name: f"/usr/bin/{name}"
if name
@ -484,11 +485,11 @@ def test_official_libreoffice_desktop_status_and_url_contract(tmp_path, monkeypa
}
else "",
)
monkeypatch.setattr(libreoffice_desktop.virtual_desktop, "XPRA_HTML_ROOT_CANDIDATES", (xpra_html,))
monkeypatch.setattr(libreoffice_desktop.virtual_desktop, "_package_installed", lambda package: True)
monkeypatch.setattr(desktop_session.virtual_desktop, "XPRA_HTML_ROOT_CANDIDATES", (xpra_html,))
monkeypatch.setattr(desktop_session.virtual_desktop, "_package_installed", lambda package: True)
status = libreoffice_desktop.collect_desktop_status()
url = libreoffice_desktop._xpra_url("abc123")
status = desktop_session.collect_desktop_status()
url = desktop_session._xpra_url("abc123")
assert status["healthy"] is True
assert status["xpra_html_root"] == str(xpra_html)
@ -504,6 +505,21 @@ def test_official_libreoffice_desktop_status_and_url_contract(tmp_path, monkeypa
assert "printing=true" in url
def test_desktop_gateway_patches_xpra_menu_script():
source = (PROJECT_ROOT / "helpers" / "virtual_desktop_routes.py").read_text(encoding="utf-8")
assert "XPRA_MENU_CUSTOM_PATCH" in source
assert 'upstream_path.endswith("/js/MenuCustom.js")' in source
assert "window.noWindowList" in source
assert "__a0SafeWindowList" in source
assert "XPRA_WINDOW_OFFSET_WARNING_PATCH" in source
assert "XPRA_WINDOW_SCRIPT_PATCH" in source
assert "a0_desktop_patch=20260506" in source
assert 'upstream_path.endswith("/index.html")' in source
assert 'upstream_path.endswith("/js/Window.js")' in source
assert "window does not fit in canvas, offsets:" in source
def test_office_session_desktop_state_action_defaults_without_screenshot(monkeypatch):
api_module = types.ModuleType("helpers.api")
@ -527,7 +543,7 @@ def test_office_session_desktop_state_action_defaults_without_screenshot(monkeyp
return {
"ok": True,
"display": ":120",
"profile_dir": "/a0/tmp/_office/desktop/profiles/agent-zero-desktop",
"profile_dir": "/a0/usr/_desktop/profiles/agent-zero-desktop",
"size": {"width": 1440, "height": 900},
"pointer": {"x": 0, "y": 0, "screen": 0, "window": 0},
"active_window": None,
@ -537,7 +553,7 @@ def test_office_session_desktop_state_action_defaults_without_screenshot(monkeyp
"errors": [],
}
monkeypatch.setattr(office_session.libreoffice_desktop, "get_manager", lambda: FakeManager())
monkeypatch.setattr(office_session.desktop_session, "get_manager", lambda: FakeManager())
handler = office_session.OfficeSession(app=None, thread_lock=None)
request = types.SimpleNamespace(headers={}, host_url="http://localhost:32080")
@ -583,7 +599,7 @@ def test_office_session_desktop_shutdown_action_calls_manager(monkeypatch):
"source": source,
}
monkeypatch.setattr(office_session.libreoffice_desktop, "get_manager", lambda: FakeManager())
monkeypatch.setattr(office_session.desktop_session, "get_manager", lambda: FakeManager())
handler = office_session.OfficeSession(app=None, thread_lock=None)
request = types.SimpleNamespace(headers={}, host_url="http://localhost:32080")
@ -600,7 +616,50 @@ def test_office_session_desktop_shutdown_action_calls_manager(monkeypatch):
monkeypatch.delattr(api_package, "office_session", raising=False)
def test_official_libreoffice_desktop_manager_opens_binary_session(office_state, tmp_path, monkeypatch):
def test_office_binary_open_requires_explicit_desktop_without_cold_session(office_state, monkeypatch):
api_module = types.ModuleType("helpers.api")
class ApiHandler:
def __init__(self, app=None, thread_lock=None):
self.app = app
self.thread_lock = thread_lock
api_module.ApiHandler = ApiHandler
api_module.Request = object
monkeypatch.setitem(sys.modules, "helpers.api", api_module)
monkeypatch.delitem(sys.modules, "plugins._office.api.office_session", raising=False)
from plugins._office.api import office_session
doc = document_store.create_document("document", "Cold Memo", "odt", "No surprise Desktop.")
def forbidden_session(*_args, **_kwargs):
raise AssertionError("cold binary open must not create a store session")
class ForbiddenManager:
def open(self, *_args, **_kwargs):
raise AssertionError("cold binary open must not open Desktop")
monkeypatch.setattr(office_session.document_store, "create_session", forbidden_session)
monkeypatch.setattr(office_session.desktop_session, "get_manager", lambda: ForbiddenManager())
handler = office_session.OfficeSession(app=None, thread_lock=None)
request = types.SimpleNamespace(headers={}, host_url="http://localhost:32080")
result = asyncio.run(handler.process({"action": "open", "file_id": doc["file_id"]}, request))
assert result["ok"] is True
assert result["requires_desktop"] is True
assert result["file_id"] == doc["file_id"]
assert "session_id" not in result
assert "store_session_id" not in result
monkeypatch.delitem(sys.modules, "plugins._office.api.office_session", raising=False)
api_package = sys.modules.get("plugins._office.api")
if api_package is not None:
monkeypatch.delattr(api_package, "office_session", raising=False)
def test_official_desktop_session_manager_opens_binary_session(office_state, tmp_path, monkeypatch):
class FakeProcess:
pid = 4242
@ -616,21 +675,21 @@ def test_official_libreoffice_desktop_manager_opens_binary_session(office_state,
def kill(self):
return None
monkeypatch.setattr(libreoffice_desktop, "STATE_DIR", tmp_path / "desktop")
monkeypatch.setattr(libreoffice_desktop, "SESSION_DIR", tmp_path / "desktop" / "sessions")
monkeypatch.setattr(libreoffice_desktop, "PROFILE_DIR", tmp_path / "desktop" / "profiles")
monkeypatch.setattr(libreoffice_desktop, "collect_desktop_status", lambda: {"healthy": True, "message": "ok"})
monkeypatch.setattr(libreoffice_desktop.libreoffice, "find_soffice", lambda: "/usr/bin/soffice")
monkeypatch.setattr(libreoffice_desktop, "_port_is_free", lambda port: True)
monkeypatch.setattr(libreoffice_desktop.virtual_desktop, "has_window", lambda **kwargs: True)
real_get_abs_path = libreoffice_desktop.files.get_abs_path
monkeypatch.setattr(desktop_session, "STATE_DIR", tmp_path / "desktop")
monkeypatch.setattr(desktop_session, "SESSION_DIR", tmp_path / "desktop" / "sessions")
monkeypatch.setattr(desktop_session, "PROFILE_DIR", tmp_path / "desktop" / "profiles")
monkeypatch.setattr(desktop_session, "collect_desktop_status", lambda: {"healthy": True, "message": "ok"})
monkeypatch.setattr(desktop_session.libreoffice, "find_soffice", lambda: "/usr/bin/soffice")
monkeypatch.setattr(desktop_session, "_port_is_free", lambda port: True)
monkeypatch.setattr(desktop_session.virtual_desktop, "has_window", lambda **kwargs: True)
real_get_abs_path = desktop_session.files.get_abs_path
def fake_get_abs_path(*parts):
if parts and parts[0] == "usr":
return str(tmp_path.joinpath(*parts))
return real_get_abs_path(*parts)
monkeypatch.setattr(libreoffice_desktop.files, "get_abs_path", fake_get_abs_path)
monkeypatch.setattr(desktop_session.files, "get_abs_path", fake_get_abs_path)
def fake_spawn(self, session):
session.profile_dir.mkdir(parents=True, exist_ok=True)
@ -639,11 +698,11 @@ def test_official_libreoffice_desktop_manager_opens_binary_session(office_state,
def fake_open_document(self, session, doc):
session.processes[f"soffice-{doc['file_id']}"] = FakeProcess()
monkeypatch.setattr(libreoffice_desktop.LibreOfficeDesktopManager, "_spawn_desktop_locked", fake_spawn)
monkeypatch.setattr(libreoffice_desktop.LibreOfficeDesktopManager, "_open_document_locked", fake_open_document)
monkeypatch.setattr(desktop_session.DesktopSessionManager, "_spawn_desktop_locked", fake_spawn)
monkeypatch.setattr(desktop_session.DesktopSessionManager, "_open_document_locked", fake_open_document)
doc = document_store.create_document("spreadsheet", "Official Sheet", "ods", "Name,Value\nA,1")
manager = libreoffice_desktop.LibreOfficeDesktopManager()
manager = desktop_session.DesktopSessionManager()
payload = manager.open(doc)
assert payload["available"] is True
@ -792,7 +851,7 @@ def test_official_libreoffice_desktop_manager_opens_binary_session(office_state,
assert "xmessage" in shutdown_script
assert '"-buttons",' in shutdown_script
desktop_helper = (
PROJECT_ROOT / "plugins" / "_office" / "helpers" / "libreoffice_desktop.py"
PROJECT_ROOT / "plugins" / "_desktop" / "helpers" / "desktop_session.py"
).read_text(encoding="utf-8")
assert "_refresh_xfce_desktop" in desktop_helper
assert "DBUS_SESSION_BUS_ADDRESS" in desktop_helper
@ -803,7 +862,7 @@ def test_official_libreoffice_desktop_manager_opens_binary_session(office_state,
/ payload["session_id"]
/ ".config"
/ "autostart"
/ "agent-zero-office-desktop.desktop"
/ "agent-zero-desktop.desktop"
)
assert "prepare-xfce-profile.sh" in autostart.read_text(encoding="utf-8")
profile_script = (
@ -841,31 +900,31 @@ def test_official_libreoffice_desktop_manager_opens_binary_session(office_state,
).read_text(encoding="utf-8")
assert "NoDisplay=true" in entry
assert "Hidden=true" in entry
assert manager.proxy_for_token(payload["token"]) == ("127.0.0.1", libreoffice_desktop.XPRA_PORT_BASE)
assert manager.proxy_for_token(payload["token"]) == ("127.0.0.1", desktop_session.XPRA_PORT_BASE)
assert manager.close(payload["session_id"], save_first=False)["closed"] == 0
assert manager.close(payload["session_id"], save_first=False)["persistent"] is True
def test_shutdown_panel_launcher_requires_second_click(tmp_path):
profile_dir = tmp_path / "desktop" / "profiles" / libreoffice_desktop.SYSTEM_SESSION_ID
profile_dir = tmp_path / "desktop" / "profiles" / desktop_session.SYSTEM_SESSION_ID
profile_dir.mkdir(parents=True)
desktop_path = tmp_path / "workdir"
desktop_path.mkdir()
session = libreoffice_desktop.DesktopSession(
session_id=libreoffice_desktop.SYSTEM_SESSION_ID,
file_id=libreoffice_desktop.SYSTEM_FILE_ID,
session = desktop_session.DesktopSession(
session_id=desktop_session.SYSTEM_SESSION_ID,
file_id=desktop_session.SYSTEM_FILE_ID,
extension="desktop",
path=str(desktop_path),
title=libreoffice_desktop.SYSTEM_TITLE,
display=libreoffice_desktop.DISPLAY_BASE,
xpra_port=libreoffice_desktop.XPRA_PORT_BASE,
token=libreoffice_desktop.SYSTEM_SESSION_ID,
title=desktop_session.SYSTEM_TITLE,
display=desktop_session.DISPLAY_BASE,
xpra_port=desktop_session.XPRA_PORT_BASE,
token=desktop_session.SYSTEM_SESSION_ID,
url="/desktop/session/agent-zero-desktop/index.html",
profile_dir=profile_dir,
)
script = libreoffice_desktop._write_shutdown_bridge_script(session)
request = libreoffice_desktop._shutdown_request_path(session)
arm = libreoffice_desktop._shutdown_arm_path(session)
script = desktop_session._write_shutdown_bridge_script(session)
request = desktop_session._shutdown_request_path(session)
arm = desktop_session._shutdown_arm_path(session)
env = dict(os.environ)
env.pop("DISPLAY", None)
@ -882,7 +941,7 @@ def test_shutdown_panel_launcher_requires_second_click(tmp_path):
assert not arm.exists()
def test_libreoffice_desktop_sync_consumes_shutdown_marker(tmp_path, monkeypatch):
def test_desktop_session_sync_consumes_shutdown_marker(tmp_path, monkeypatch):
class FakeProcess:
pid = 5252
terminated = False
@ -900,32 +959,32 @@ def test_libreoffice_desktop_sync_consumes_shutdown_marker(tmp_path, monkeypatch
def kill(self):
self.terminated = True
monkeypatch.setattr(libreoffice_desktop, "STATE_DIR", tmp_path / "desktop")
monkeypatch.setattr(libreoffice_desktop, "SESSION_DIR", tmp_path / "desktop" / "sessions")
monkeypatch.setattr(libreoffice_desktop, "PROFILE_DIR", tmp_path / "desktop" / "profiles")
monkeypatch.setattr(desktop_session, "STATE_DIR", tmp_path / "desktop")
monkeypatch.setattr(desktop_session, "SESSION_DIR", tmp_path / "desktop" / "sessions")
monkeypatch.setattr(desktop_session, "PROFILE_DIR", tmp_path / "desktop" / "profiles")
profile_dir = tmp_path / "desktop" / "profiles" / libreoffice_desktop.SYSTEM_SESSION_ID
profile_dir = tmp_path / "desktop" / "profiles" / desktop_session.SYSTEM_SESSION_ID
profile_dir.mkdir(parents=True)
desktop_path = tmp_path / "workdir"
desktop_path.mkdir()
session = libreoffice_desktop.DesktopSession(
session_id=libreoffice_desktop.SYSTEM_SESSION_ID,
file_id=libreoffice_desktop.SYSTEM_FILE_ID,
session = desktop_session.DesktopSession(
session_id=desktop_session.SYSTEM_SESSION_ID,
file_id=desktop_session.SYSTEM_FILE_ID,
extension="desktop",
path=str(desktop_path),
title=libreoffice_desktop.SYSTEM_TITLE,
display=libreoffice_desktop.DISPLAY_BASE,
xpra_port=libreoffice_desktop.XPRA_PORT_BASE,
token=libreoffice_desktop.SYSTEM_SESSION_ID,
title=desktop_session.SYSTEM_TITLE,
display=desktop_session.DISPLAY_BASE,
xpra_port=desktop_session.XPRA_PORT_BASE,
token=desktop_session.SYSTEM_SESSION_ID,
url="/desktop/session/agent-zero-desktop/index.html",
profile_dir=profile_dir,
processes={"xpra": FakeProcess()},
)
manager = libreoffice_desktop.LibreOfficeDesktopManager()
manager = desktop_session.DesktopSessionManager()
manager._sessions[session.session_id] = session
manager._write_manifest(session)
libreoffice_desktop._write_url_bridge_script(session)
shutdown_request = libreoffice_desktop._shutdown_request_path(session)
desktop_session._write_url_bridge_script(session)
shutdown_request = desktop_session._shutdown_request_path(session)
shutdown_request.write_text('{"source": "tray", "created_at": 123.0}\n', encoding="utf-8")
save_calls = []
monkeypatch.setattr(
@ -940,56 +999,177 @@ def test_libreoffice_desktop_sync_consumes_shutdown_marker(tmp_path, monkeypatch
assert result["intentional_shutdown"] is True
assert result["source"] == "tray"
assert result["closed"] == 1
assert save_calls == [(libreoffice_desktop.SYSTEM_SESSION_ID, "")]
assert save_calls == [(desktop_session.SYSTEM_SESSION_ID, "")]
assert not shutdown_request.exists()
assert not (libreoffice_desktop.SESSION_DIR / f"{session.session_id}.json").exists()
assert not (desktop_session.SESSION_DIR / f"{session.session_id}.json").exists()
assert manager.get(session.session_id) is None
def test_libreoffice_desktop_cleanup_preserves_live_owner_manifest(tmp_path, monkeypatch):
def test_desktop_session_cleanup_preserves_live_owner_manifest(tmp_path, monkeypatch):
session_dir = tmp_path / "sessions"
legacy_session_dir = tmp_path / "legacy-sessions"
session_dir.mkdir()
legacy_session_dir.mkdir()
manifest = session_dir / "live.json"
manifest.write_text(
json.dumps({"owner_pid": os.getpid(), "pids": {"xpra": 987654}}),
encoding="utf-8",
)
monkeypatch.setattr(libreoffice_desktop, "SESSION_DIR", session_dir)
legacy_manifest = legacy_session_dir / "stale.json"
legacy_manifest.write_text(
json.dumps({"owner_pid": 987650, "pids": {"xpra": 987651, "xfce": 987652}}),
encoding="utf-8",
)
monkeypatch.setattr(desktop_session, "SESSION_DIR", session_dir)
monkeypatch.setattr(desktop_session, "LEGACY_SESSION_DIRS", (legacy_session_dir,))
killed = []
def fake_kill_pid(pid):
killed.append(pid)
return True
monkeypatch.setattr(
libreoffice_desktop,
desktop_session,
"_kill_pid",
lambda _pid: pytest.fail("cleanup should not kill a desktop owned by a live UI process"),
fake_kill_pid,
)
result = libreoffice_desktop.cleanup_stale_runtime_state()
result = desktop_session.cleanup_stale_runtime_state()
assert result["killed"] == []
assert result["killed"] == [987651, 987652]
assert killed == [987651, 987652]
assert manifest.exists()
assert not legacy_manifest.exists()
def test_libreoffice_desktop_removes_stale_lock_file(tmp_path):
def test_desktop_session_removes_stale_lock_file(tmp_path):
doc_path = tmp_path / "Deck.pptx"
doc_path.write_text("pptx", encoding="utf-8")
lock_path = tmp_path / ".~lock.Deck.pptx#"
lock_path.write_text("stale", encoding="utf-8")
session = libreoffice_desktop.DesktopSession(
session = desktop_session.DesktopSession(
session_id="session",
file_id="file",
extension="pptx",
path=str(doc_path),
title=doc_path.name,
display=libreoffice_desktop.DISPLAY_BASE,
xpra_port=libreoffice_desktop.XPRA_PORT_BASE,
display=desktop_session.DISPLAY_BASE,
xpra_port=desktop_session.XPRA_PORT_BASE,
token="token",
url="/desktop/session/token/index.html",
profile_dir=tmp_path / "profile",
)
libreoffice_desktop.LibreOfficeDesktopManager()._remove_stale_lock_file(session)
desktop_session.DesktopSessionManager()._remove_stale_lock_file(session)
assert not lock_path.exists()
def _isolate_office_cleanup_hook(monkeypatch, tmp_path):
monkeypatch.setattr(hooks, "RETIRED_WEB_APT_SOURCE_FILE", tmp_path / "missing.sources")
monkeypatch.setattr(hooks, "RETIRED_WEB_APT_KEYRING_FILE", tmp_path / "missing.gpg")
monkeypatch.setattr(hooks, "RETIRED_WEB_SUPERVISOR_FILE", tmp_path / "missing.conf")
monkeypatch.setattr(hooks, "RETIRED_WEB_RUNTIME_DIRS", [])
monkeypatch.setattr(hooks, "CLEANUP_MARKER", tmp_path / "state" / "cleanup.done")
monkeypatch.setattr(hooks, "_installed_packages", lambda packages: [])
monkeypatch.setattr(hooks, "_kill_old_processes", lambda errors: None)
monkeypatch.setattr(hooks, "_ensure_runtime_dependencies", lambda installed, errors: None)
monkeypatch.setattr(hooks, "_purge_packages", lambda removed, errors, **kwargs: None)
monkeypatch.setattr(hooks.shutil, "which", lambda name: "")
def test_cleanup_hook_migrates_legacy_document_state_without_removing_source(tmp_path, monkeypatch):
_isolate_office_cleanup_hook(monkeypatch, tmp_path)
legacy_documents = tmp_path / "usr" / "plugins" / "_office" / "documents"
document_state = tmp_path / "usr" / "_office" / "documents"
legacy_documents.mkdir(parents=True)
(legacy_documents / "documents.sqlite3").write_text("legacy-db\n", encoding="utf-8")
(legacy_documents / "backups").mkdir()
(legacy_documents / "backups" / "draft.md").write_text("backup\n", encoding="utf-8")
monkeypatch.setattr(hooks, "DOCUMENT_STATE_DIR", document_state)
monkeypatch.setattr(hooks, "LEGACY_DOCUMENT_STATE_DIRS", [legacy_documents])
monkeypatch.setattr(hooks, "_ensure_desktop_runtime_compat", lambda installed, removed, warnings, errors: None)
result = hooks.cleanup_stale_runtime_state(force=True)
assert result["ok"] is True
assert result["migrated"] == [f"{legacy_documents} -> {document_state}"]
assert (legacy_documents / "documents.sqlite3").exists()
assert (document_state / "documents.sqlite3").read_text(encoding="utf-8") == "legacy-db\n"
assert (document_state / "backups" / "draft.md").read_text(encoding="utf-8") == "backup\n"
def test_cleanup_hook_prefers_existing_new_document_state_without_merge(tmp_path, monkeypatch):
_isolate_office_cleanup_hook(monkeypatch, tmp_path)
legacy_documents = tmp_path / "legacy-documents"
document_state = tmp_path / "usr" / "_office" / "documents"
legacy_documents.mkdir(parents=True)
document_state.mkdir(parents=True)
(legacy_documents / "documents.sqlite3").write_text("legacy-db\n", encoding="utf-8")
(document_state / "documents.sqlite3").write_text("new-db\n", encoding="utf-8")
monkeypatch.setattr(hooks, "DOCUMENT_STATE_DIR", document_state)
monkeypatch.setattr(hooks, "LEGACY_DOCUMENT_STATE_DIRS", [legacy_documents])
monkeypatch.setattr(hooks, "_ensure_desktop_runtime_compat", lambda installed, removed, warnings, errors: None)
result = hooks.cleanup_stale_runtime_state(force=True)
assert result["ok"] is True
assert result["migrated"] == []
assert result["warnings"] == [
f"Legacy Office document state left in place because {document_state} already exists: {legacy_documents}"
]
assert (legacy_documents / "documents.sqlite3").read_text(encoding="utf-8") == "legacy-db\n"
assert (document_state / "documents.sqlite3").read_text(encoding="utf-8") == "new-db\n"
def test_office_hook_desktop_compat_forwards_runtime_result(monkeypatch):
monkeypatch.setattr(
desktop_hooks,
"cleanup_stale_runtime_state",
lambda: {
"installed": ["xpra-server"],
"removed": ["firefox-esr"],
"warnings": ["desktop warning"],
"errors": ["desktop error"],
},
)
installed = []
removed = []
warnings = []
errors = []
hooks._ensure_desktop_runtime_compat(installed, removed, warnings, errors)
assert installed == ["xpra-server"]
assert removed == ["firefox-esr"]
assert warnings == ["desktop warning"]
assert errors == ["desktop error"]
def test_cleanup_hook_delegates_desktop_runtime_for_legacy_self_update(tmp_path, monkeypatch):
_isolate_office_cleanup_hook(monkeypatch, tmp_path)
monkeypatch.setattr(hooks, "DOCUMENT_STATE_DIR", tmp_path / "usr" / "_office" / "documents")
monkeypatch.setattr(hooks, "LEGACY_DOCUMENT_STATE_DIRS", [])
calls = []
def fake_desktop_compat(installed, removed, warnings, errors):
calls.append("desktop")
installed.append("xpra-server")
removed.append("firefox-esr")
warnings.append("desktop runtime prepared through office compatibility hook")
monkeypatch.setattr(hooks, "_ensure_desktop_runtime_compat", fake_desktop_compat)
result = hooks.cleanup_stale_runtime_state(force=True)
assert calls == ["desktop"]
assert result["installed"] == ["xpra-server"]
assert result["removed"] == ["firefox-esr"]
assert result["warnings"] == ["desktop runtime prepared through office compatibility hook"]
def test_cleanup_hook_removes_stale_runtime_state_idempotently(tmp_path, monkeypatch):
source = tmp_path / "sources.list.d" / "retired.sources"
keyring = tmp_path / "keyrings" / "retired.gpg"
@ -1003,17 +1183,20 @@ def test_cleanup_hook_removes_stale_runtime_state_idempotently(tmp_path, monkeyp
(runtime_dir / "nested").mkdir(parents=True, exist_ok=True)
(runtime_dir / "nested" / "state.txt").write_text("old\n", encoding="utf-8")
monkeypatch.setattr(hooks, "APT_SOURCE_FILE", source)
monkeypatch.setattr(hooks, "APT_KEYRING_FILE", keyring)
monkeypatch.setattr(hooks, "SUPERVISOR_FILE", supervisor)
monkeypatch.setattr(hooks, "RUNTIME_DIRS", [runtime_dir])
monkeypatch.setattr(hooks, "RETIRED_WEB_APT_SOURCE_FILE", source)
monkeypatch.setattr(hooks, "RETIRED_WEB_APT_KEYRING_FILE", keyring)
monkeypatch.setattr(hooks, "RETIRED_WEB_SUPERVISOR_FILE", supervisor)
monkeypatch.setattr(hooks, "RETIRED_WEB_RUNTIME_DIRS", [runtime_dir])
monkeypatch.setattr(hooks, "CLEANUP_MARKER", marker)
monkeypatch.setattr(hooks, "DOCUMENT_STATE_DIR", tmp_path / "usr" / "_office" / "documents")
monkeypatch.setattr(hooks, "LEGACY_DOCUMENT_STATE_DIRS", [])
monkeypatch.setattr(hooks, "_installed_packages", lambda packages: [])
monkeypatch.setattr(hooks, "_kill_old_processes", lambda errors: None)
monkeypatch.setattr(hooks, "_ensure_desktop_runtime_compat", lambda installed, removed, warnings, errors: None)
def fake_ensure(installed, errors):
assert not source.exists()
installed.append("xpra")
installed.append("libreoffice-core")
def fake_purge(removed, errors, **kwargs):
return None
@ -1026,7 +1209,7 @@ def test_cleanup_hook_removes_stale_runtime_state_idempotently(tmp_path, monkeyp
skipped = hooks.cleanup_stale_runtime_state()
assert first["ok"] is True
assert first["installed"] == ["xpra"]
assert first["installed"] == ["libreoffice-core"]
assert second["ok"] is True
assert skipped["skipped"] is True
assert not source.exists()
@ -1037,12 +1220,8 @@ def test_cleanup_hook_removes_stale_runtime_state_idempotently(tmp_path, monkeyp
def test_office_startup_defers_persistent_desktop_runtime(monkeypatch):
calls = []
cleanup_calls = []
started_threads = []
routes_module = types.ModuleType("plugins._office.helpers.libreoffice_desktop_routes")
routes_module.install_route_hooks = lambda: calls.append("routes")
monkeypatch.setitem(sys.modules, "plugins._office.helpers.libreoffice_desktop_routes", routes_module)
monkeypatch.delitem(
sys.modules,
"plugins._office.extensions.python.startup_migration._20_office_routes",
@ -1073,12 +1252,11 @@ def test_office_startup_defers_persistent_desktop_runtime(monkeypatch):
office_startup.OfficeStartupCleanup(agent=None).execute()
assert calls == ["routes"]
assert cleanup_calls == []
assert len(started_threads) == 1
assert started_threads[0].name == "a0-office-runtime-preparation"
assert started_threads[0].name == "a0-office-document-runtime-preparation"
assert started_threads[0].daemon is True
assert not hasattr(office_startup, "libreoffice_desktop")
assert not hasattr(office_startup, "desktop_session")
started_threads[0].target()
assert cleanup_calls == ["cleanup"]
@ -1089,14 +1267,17 @@ def test_cleanup_hook_reruns_when_stale_packages_exist_after_old_marker(tmp_path
marker.parent.mkdir(parents=True)
marker.write_text("old\n", encoding="utf-8")
monkeypatch.setattr(hooks, "APT_SOURCE_FILE", tmp_path / "missing.sources")
monkeypatch.setattr(hooks, "APT_KEYRING_FILE", tmp_path / "missing.gpg")
monkeypatch.setattr(hooks, "SUPERVISOR_FILE", tmp_path / "missing.conf")
monkeypatch.setattr(hooks, "RUNTIME_DIRS", [])
monkeypatch.setattr(hooks, "RETIRED_WEB_APT_SOURCE_FILE", tmp_path / "missing.sources")
monkeypatch.setattr(hooks, "RETIRED_WEB_APT_KEYRING_FILE", tmp_path / "missing.gpg")
monkeypatch.setattr(hooks, "RETIRED_WEB_SUPERVISOR_FILE", tmp_path / "missing.conf")
monkeypatch.setattr(hooks, "RETIRED_WEB_RUNTIME_DIRS", [])
monkeypatch.setattr(hooks, "CLEANUP_MARKER", marker)
monkeypatch.setattr(hooks, "DOCUMENT_STATE_DIR", tmp_path / "usr" / "_office" / "documents")
monkeypatch.setattr(hooks, "LEGACY_DOCUMENT_STATE_DIRS", [])
monkeypatch.setattr(hooks, "_installed_packages", lambda packages: ["coolwsd"])
monkeypatch.setattr(hooks, "_ensure_runtime_dependencies", lambda installed, errors: None)
monkeypatch.setattr(hooks, "_kill_old_processes", lambda errors: None)
monkeypatch.setattr(hooks, "_ensure_desktop_runtime_compat", lambda installed, removed, warnings, errors: None)
def fake_purge(removed, errors, **kwargs):
removed.extend(kwargs["installed_packages"])
@ -1115,19 +1296,21 @@ def test_cleanup_hook_removes_retired_supervisor_program_after_marker(tmp_path,
marker.write_text("ok\n", encoding="utf-8")
calls = []
monkeypatch.setattr(hooks, "APT_SOURCE_FILE", tmp_path / "missing.sources")
monkeypatch.setattr(hooks, "APT_KEYRING_FILE", tmp_path / "missing.gpg")
monkeypatch.setattr(hooks, "SUPERVISOR_FILE", tmp_path / "missing.conf")
monkeypatch.setattr(hooks, "RUNTIME_DIRS", [])
monkeypatch.setattr(hooks, "RETIRED_WEB_APT_SOURCE_FILE", tmp_path / "missing.sources")
monkeypatch.setattr(hooks, "RETIRED_WEB_APT_KEYRING_FILE", tmp_path / "missing.gpg")
monkeypatch.setattr(hooks, "RETIRED_WEB_SUPERVISOR_FILE", tmp_path / "missing.conf")
monkeypatch.setattr(hooks, "RETIRED_WEB_RUNTIME_DIRS", [])
monkeypatch.setattr(hooks, "CLEANUP_MARKER", marker)
monkeypatch.setattr(hooks, "DOCUMENT_STATE_DIR", tmp_path / "usr" / "_office" / "documents")
monkeypatch.setattr(hooks, "LEGACY_DOCUMENT_STATE_DIRS", [])
monkeypatch.setattr(hooks, "_installed_packages", lambda packages: [])
monkeypatch.setattr(hooks, "_ensure_runtime_dependencies", lambda installed, errors: None)
monkeypatch.setattr(hooks, "_cleanup_desktop_sessions", lambda errors: None)
monkeypatch.setattr(hooks, "_ensure_desktop_runtime_compat", lambda installed, removed, warnings, errors: None)
monkeypatch.setattr(hooks.shutil, "which", lambda name: "/usr/bin/supervisorctl" if name == "supervisorctl" else "")
def fake_supervisorctl(*args):
calls.append(args)
if args == ("status", hooks.SUPERVISOR_PROGRAM):
if args == ("status", hooks.RETIRED_WEB_SUPERVISOR_PROGRAM):
return types.SimpleNamespace(
returncode=0,
stdout="a0_office_collabora BACKOFF can't find command\n",
@ -1143,22 +1326,22 @@ def test_cleanup_hook_removes_retired_supervisor_program_after_marker(tmp_path,
assert result["skipped"] is True
assert result["errors"] == []
assert calls == [
("status", hooks.SUPERVISOR_PROGRAM),
("stop", hooks.SUPERVISOR_PROGRAM),
("remove", hooks.SUPERVISOR_PROGRAM),
("status", hooks.RETIRED_WEB_SUPERVISOR_PROGRAM),
("stop", hooks.RETIRED_WEB_SUPERVISOR_PROGRAM),
("remove", hooks.RETIRED_WEB_SUPERVISOR_PROGRAM),
("reread",),
("update",),
]
def test_cleanup_hook_installs_missing_libreoffice_desktop_dependencies(monkeypatch):
def test_cleanup_hook_installs_missing_desktop_session_dependencies(monkeypatch):
calls = []
installed_state = {"xpra": False}
monkeypatch.setattr(hooks.os, "geteuid", lambda: 0)
monkeypatch.setattr(hooks.shutil, "which", lambda name: f"/usr/bin/{name}" if name in {"apt-get", "dpkg-query"} else "")
monkeypatch.setattr(hooks, "RUNTIME_PACKAGES", ("xpra",))
monkeypatch.setattr(hooks, "_package_installed", lambda package: installed_state.get(package, False))
monkeypatch.setattr(desktop_hooks.os, "geteuid", lambda: 0)
monkeypatch.setattr(desktop_hooks.shutil, "which", lambda name: f"/usr/bin/{name}" if name in {"apt-get", "dpkg-query"} else "")
monkeypatch.setattr(desktop_hooks, "RUNTIME_PACKAGES", ("xpra",))
monkeypatch.setattr(desktop_hooks, "_package_installed", lambda package: installed_state.get(package, False))
def fake_run(command, **kwargs):
calls.append(command)
@ -1166,11 +1349,11 @@ def test_cleanup_hook_installs_missing_libreoffice_desktop_dependencies(monkeypa
installed_state["xpra"] = True
return types.SimpleNamespace(returncode=0, stdout="", stderr="")
monkeypatch.setattr(hooks.subprocess, "run", fake_run)
monkeypatch.setattr(desktop_hooks.subprocess, "run", fake_run)
installed = []
errors = []
hooks._ensure_runtime_dependencies(installed, errors)
desktop_hooks._ensure_runtime_dependencies(installed, errors)
assert installed == ["xpra"]
assert errors == []
@ -1184,19 +1367,19 @@ def test_cleanup_hook_enables_official_xpra_repo_when_kali_lacks_candidate(tmp_p
keyring = tmp_path / "keyrings" / "xpra.asc"
source = tmp_path / "sources.list.d" / "xpra.sources"
monkeypatch.setattr(hooks.os, "geteuid", lambda: 0)
monkeypatch.setattr(desktop_hooks.os, "geteuid", lambda: 0)
monkeypatch.setattr(
hooks.shutil,
desktop_hooks.shutil,
"which",
lambda name: f"/usr/bin/{name}" if name in {"apt-get", "dpkg-query", "apt-cache"} else "",
)
monkeypatch.setattr(hooks, "RUNTIME_PACKAGES", ("xpra",))
monkeypatch.setattr(hooks, "XPRA_KEYRING_FILE", keyring)
monkeypatch.setattr(hooks, "XPRA_SOURCE_FILE", source)
monkeypatch.setattr(hooks, "_download", lambda url: b"xpra-key")
monkeypatch.setattr(hooks, "_read_os_release", lambda: {"ID": "kali", "VERSION_CODENAME": "kali-rolling"})
monkeypatch.setattr(hooks, "_dpkg_architecture", lambda: "amd64")
monkeypatch.setattr(hooks, "_package_installed", lambda package: installed_state.get(package, False))
monkeypatch.setattr(desktop_hooks, "RUNTIME_PACKAGES", ("xpra",))
monkeypatch.setattr(desktop_hooks, "XPRA_KEYRING_FILE", keyring)
monkeypatch.setattr(desktop_hooks, "XPRA_SOURCE_FILE", source)
monkeypatch.setattr(desktop_hooks, "_download", lambda url: b"xpra-key")
monkeypatch.setattr(desktop_hooks, "_read_os_release", lambda: {"ID": "kali", "VERSION_CODENAME": "kali-rolling"})
monkeypatch.setattr(desktop_hooks, "_dpkg_architecture", lambda: "amd64")
monkeypatch.setattr(desktop_hooks, "_package_installed", lambda package: installed_state.get(package, False))
def fake_run(command, **kwargs):
calls.append(command)
@ -1206,11 +1389,11 @@ def test_cleanup_hook_enables_official_xpra_repo_when_kali_lacks_candidate(tmp_p
installed_state["xpra"] = True
return types.SimpleNamespace(returncode=0, stdout="", stderr="")
monkeypatch.setattr(hooks.subprocess, "run", fake_run)
monkeypatch.setattr(desktop_hooks.subprocess, "run", fake_run)
installed = []
errors = []
hooks._ensure_runtime_dependencies(installed, errors)
desktop_hooks._ensure_runtime_dependencies(installed, errors)
assert errors == []
assert installed == ["xpra"]
@ -1227,19 +1410,19 @@ def test_cleanup_hook_uses_trixie_xpra_components_for_kali_arm64(tmp_path, monke
keyring = tmp_path / "keyrings" / "xpra.asc"
source = tmp_path / "sources.list.d" / "xpra.sources"
monkeypatch.setattr(hooks.os, "geteuid", lambda: 0)
monkeypatch.setattr(desktop_hooks.os, "geteuid", lambda: 0)
monkeypatch.setattr(
hooks.shutil,
desktop_hooks.shutil,
"which",
lambda name: f"/usr/bin/{name}" if name in {"apt-get", "dpkg-query", "apt-cache"} else "",
)
monkeypatch.setattr(hooks, "RUNTIME_PACKAGES", ("xpra-server", "xpra-x11", "xpra-html5"))
monkeypatch.setattr(hooks, "XPRA_KEYRING_FILE", keyring)
monkeypatch.setattr(hooks, "XPRA_SOURCE_FILE", source)
monkeypatch.setattr(hooks, "_download", lambda url: b"xpra-key")
monkeypatch.setattr(hooks, "_read_os_release", lambda: {"ID": "kali", "VERSION_CODENAME": "kali-rolling"})
monkeypatch.setattr(hooks, "_dpkg_architecture", lambda: "arm64")
monkeypatch.setattr(hooks, "_package_installed", lambda package: installed_state.get(package, False))
monkeypatch.setattr(desktop_hooks, "RUNTIME_PACKAGES", ("xpra-server", "xpra-x11", "xpra-html5"))
monkeypatch.setattr(desktop_hooks, "XPRA_KEYRING_FILE", keyring)
monkeypatch.setattr(desktop_hooks, "XPRA_SOURCE_FILE", source)
monkeypatch.setattr(desktop_hooks, "_download", lambda url: b"xpra-key")
monkeypatch.setattr(desktop_hooks, "_read_os_release", lambda: {"ID": "kali", "VERSION_CODENAME": "kali-rolling"})
monkeypatch.setattr(desktop_hooks, "_dpkg_architecture", lambda: "arm64")
monkeypatch.setattr(desktop_hooks, "_package_installed", lambda package: installed_state.get(package, False))
def fake_run(command, **kwargs):
calls.append(command)
@ -1250,11 +1433,11 @@ def test_cleanup_hook_uses_trixie_xpra_components_for_kali_arm64(tmp_path, monke
installed_state[package] = True
return types.SimpleNamespace(returncode=0, stdout="", stderr="")
monkeypatch.setattr(hooks.subprocess, "run", fake_run)
monkeypatch.setattr(desktop_hooks.subprocess, "run", fake_run)
installed = []
errors = []
hooks._ensure_runtime_dependencies(installed, errors)
desktop_hooks._ensure_runtime_dependencies(installed, errors)
assert errors == []
assert installed == ["xpra-server", "xpra-x11", "xpra-html5"]
@ -1281,18 +1464,18 @@ def test_cleanup_hook_skips_optional_xpra_client_codec_conflict(monkeypatch):
" but none of the choices are installable: [no choices]"
)
monkeypatch.setattr(hooks.os, "geteuid", lambda: 0)
monkeypatch.setattr(desktop_hooks.os, "geteuid", lambda: 0)
monkeypatch.setattr(
hooks.shutil,
desktop_hooks.shutil,
"which",
lambda name: f"/usr/bin/{name}" if name in {"apt-get", "dpkg-query", "apt-cache"} else "",
)
monkeypatch.setattr(
hooks,
desktop_hooks,
"RUNTIME_PACKAGES",
("xpra-server", "xpra-client", "xpra-client-gtk3", "xpra-x11", "xpra-html5"),
)
monkeypatch.setattr(hooks, "_package_installed", lambda package: installed_state.get(package, False))
monkeypatch.setattr(desktop_hooks, "_package_installed", lambda package: installed_state.get(package, False))
def fake_run(command, **kwargs):
calls.append(command)
@ -1302,11 +1485,11 @@ def test_cleanup_hook_skips_optional_xpra_client_codec_conflict(monkeypatch):
return types.SimpleNamespace(returncode=100, stdout="", stderr=codec_error)
return types.SimpleNamespace(returncode=0, stdout="", stderr="")
monkeypatch.setattr(hooks.subprocess, "run", fake_run)
monkeypatch.setattr(desktop_hooks.subprocess, "run", fake_run)
installed = []
errors = []
hooks._ensure_runtime_dependencies(installed, errors)
desktop_hooks._ensure_runtime_dependencies(installed, errors)
assert installed == []
assert errors == []
@ -1321,14 +1504,14 @@ def test_cleanup_hook_reports_required_xpra_codec_conflict(monkeypatch):
" but none of the choices are installable: [no choices]"
)
monkeypatch.setattr(hooks.os, "geteuid", lambda: 0)
monkeypatch.setattr(desktop_hooks.os, "geteuid", lambda: 0)
monkeypatch.setattr(
hooks.shutil,
desktop_hooks.shutil,
"which",
lambda name: f"/usr/bin/{name}" if name in {"apt-get", "dpkg-query", "apt-cache"} else "",
)
monkeypatch.setattr(hooks, "RUNTIME_PACKAGES", ("xpra-server",))
monkeypatch.setattr(hooks, "_package_installed", lambda package: False)
monkeypatch.setattr(desktop_hooks, "RUNTIME_PACKAGES", ("xpra-server",))
monkeypatch.setattr(desktop_hooks, "_package_installed", lambda package: False)
def fake_run(command, **kwargs):
if command[:2] == ["apt-cache", "policy"]:
@ -1337,11 +1520,11 @@ def test_cleanup_hook_reports_required_xpra_codec_conflict(monkeypatch):
return types.SimpleNamespace(returncode=100, stdout="", stderr=codec_error)
return types.SimpleNamespace(returncode=0, stdout="", stderr="")
monkeypatch.setattr(hooks.subprocess, "run", fake_run)
monkeypatch.setattr(desktop_hooks.subprocess, "run", fake_run)
installed = []
errors = []
hooks._ensure_runtime_dependencies(installed, errors)
desktop_hooks._ensure_runtime_dependencies(installed, errors)
assert installed == []
assert errors == [codec_error]

View file

@ -8,6 +8,15 @@ import pytest
PROJECT_ROOT = Path(__file__).resolve().parents[1]
SKILLS_HELPER_PATH = PROJECT_ROOT / "helpers" / "skills.py"
HELPER_STUB_MODULES = (
"helpers",
"helpers.files",
"helpers.projects",
"helpers.plugins",
"helpers.subagents",
"helpers.file_tree",
"helpers.runtime",
)
def _register_helpers_stubs():
@ -61,13 +70,22 @@ def _register_helpers_stubs():
def _load_skills_helper_module():
missing = object()
original_modules = {name: sys.modules.get(name, missing) for name in HELPER_STUB_MODULES}
_register_helpers_stubs()
spec = importlib.util.spec_from_file_location("test_skills_helper_module", SKILLS_HELPER_PATH)
module = importlib.util.module_from_spec(spec)
assert spec and spec.loader
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
try:
spec = importlib.util.spec_from_file_location("test_skills_helper_module", SKILLS_HELPER_PATH)
module = importlib.util.module_from_spec(spec)
assert spec and spec.loader
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
finally:
for name, original in original_modules.items():
if original is missing:
sys.modules.pop(name, None)
else:
sys.modules[name] = original
runtime = _load_skills_helper_module()
@ -191,6 +209,73 @@ def test_loaded_skill_entries_come_from_agent_data():
]
def test_skill_runtime_does_not_alias_old_office_skill_references():
entries = runtime.normalize_active_skills(
[
"office-artifacts",
{"name": "word-documents"},
{"path": "/a0/plugins/_office/skills/excel-workbooks"},
{"name": "Desktop", "path": "/a0/plugins/_office/skills/linux-desktop"},
"presentation-decks",
]
)
assert entries == [
{"name": "office-artifacts"},
{"name": "word-documents"},
{"path": "/a0/plugins/_office/skills/excel-workbooks"},
{"name": "Desktop", "path": "/a0/plugins/_office/skills/linux-desktop"},
{"name": "presentation-decks"},
]
agent = DummyAgent()
agent.data[runtime.AGENT_DATA_NAME_LOADED_SKILLS] = [
"office-artifacts",
"word-documents",
"excel-workbooks",
"presentation-decks",
]
assert runtime.get_loaded_skill_entries(agent) == [
{"name": "office-artifacts"},
{"name": "word-documents"},
{"name": "excel-workbooks"},
{"name": "presentation-decks"},
]
assert runtime.unload_agent_skill(agent, {"name": "office-artifacts"}) is True
assert agent.data[runtime.AGENT_DATA_NAME_LOADED_SKILLS] == [
"word-documents",
"excel-workbooks",
"presentation-decks",
]
def test_builtin_plugin_skill_delete_is_rejected_before_filesystem_delete():
with pytest.raises(PermissionError, match="Built-in plugin skills cannot be deleted"):
runtime.delete_skill("/a0/plugins/_office/skills/document-artifacts")
def test_invalid_skill_frontmatter_reports_yaml_errors():
frontmatter, errors = runtime.parse_frontmatter("name: [unterminated\n")
assert frontmatter == {}
assert errors
assert errors[0].startswith("Invalid YAML frontmatter")
def test_a0_manage_plugin_skill_frontmatter_is_valid_yaml():
text = (PROJECT_ROOT / "skills" / "a0-manage-plugin" / "SKILL.md").read_text(
encoding="utf-8"
)
frontmatter, body, errors = runtime.split_frontmatter(text)
assert errors == []
assert frontmatter["name"] == "a0-manage-plugin"
assert "Agent Zero Plugin Management" in body
def test_unload_agent_skill_removes_loaded_skill_by_name():
agent = DummyAgent()
agent.data[runtime.AGENT_DATA_NAME_LOADED_SKILLS] = [