portacode 2.2.20.dev2__py3-none-any.whl → 2.2.20.dev4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- portacode/_version.py +2 -2
- portacode/browser_cli.py +44 -4
- portacode/connection/handlers/__init__.py +2 -1
- portacode/connection/handlers/browser_handlers.py +96 -3
- portacode/connection/handlers/system_handlers.py +2 -0
- portacode/connection/handlers/test_browser_handlers.py +57 -1
- portacode/connection/terminal.py +2 -0
- portacode/tests/test_browser_cli.py +20 -0
- portacode/tests/test_system_handlers.py +15 -0
- {portacode-2.2.20.dev2.dist-info → portacode-2.2.20.dev4.dist-info}/METADATA +1 -1
- {portacode-2.2.20.dev2.dist-info → portacode-2.2.20.dev4.dist-info}/RECORD +15 -15
- {portacode-2.2.20.dev2.dist-info → portacode-2.2.20.dev4.dist-info}/WHEEL +0 -0
- {portacode-2.2.20.dev2.dist-info → portacode-2.2.20.dev4.dist-info}/entry_points.txt +0 -0
- {portacode-2.2.20.dev2.dist-info → portacode-2.2.20.dev4.dist-info}/licenses/LICENSE +0 -0
- {portacode-2.2.20.dev2.dist-info → portacode-2.2.20.dev4.dist-info}/top_level.txt +0 -0
portacode/_version.py
CHANGED
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '2.2.20.
|
|
22
|
-
__version_tuple__ = version_tuple = (2, 2, 20, '
|
|
21
|
+
__version__ = version = '2.2.20.dev4'
|
|
22
|
+
__version_tuple__ = version_tuple = (2, 2, 20, 'dev4')
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
portacode/browser_cli.py
CHANGED
|
@@ -2,17 +2,33 @@
|
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
import json
|
|
5
|
+
import os
|
|
5
6
|
import re
|
|
6
7
|
import time
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
|
|
9
10
|
import click
|
|
10
11
|
|
|
12
|
+
from .connection.handlers.browser_handlers import load_browser_config, update_browser_config
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
_MANAGED_BROWSERS_PATH = Path("/opt/ms-playwright")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _configure_managed_browser_path() -> None:
|
|
19
|
+
if _MANAGED_BROWSERS_PATH.is_dir():
|
|
20
|
+
os.environ.setdefault("PLAYWRIGHT_BROWSERS_PATH", str(_MANAGED_BROWSERS_PATH))
|
|
21
|
+
|
|
11
22
|
|
|
12
23
|
def run_browser_script(*, url: str, script: str, output_dir: Path, record_video: bool,
|
|
13
|
-
width: int, height: int, timeout_ms: int) -> dict:
|
|
24
|
+
width: int | None, height: int | None, timeout_ms: int) -> dict:
|
|
25
|
+
_configure_managed_browser_path()
|
|
14
26
|
from playwright.sync_api import sync_playwright
|
|
15
27
|
|
|
28
|
+
browser_config = load_browser_config()
|
|
29
|
+
width = width or browser_config["viewport"]["width"]
|
|
30
|
+
height = height or browser_config["viewport"]["height"]
|
|
31
|
+
|
|
16
32
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
17
33
|
shots: list[str] = []
|
|
18
34
|
values: list[object] = []
|
|
@@ -29,7 +45,8 @@ def run_browser_script(*, url: str, script: str, output_dir: Path, record_video:
|
|
|
29
45
|
context_args = {"viewport": {"width": width, "height": height}}
|
|
30
46
|
if record_video:
|
|
31
47
|
context_args.update(record_video_dir=str(video_dir), record_video_size={
|
|
32
|
-
"width":
|
|
48
|
+
"width": browser_config["video"]["width"],
|
|
49
|
+
"height": browser_config["video"]["height"],
|
|
33
50
|
})
|
|
34
51
|
context = browser.new_context(**context_args)
|
|
35
52
|
page = context.new_page()
|
|
@@ -90,8 +107,8 @@ def browser_group():
|
|
|
90
107
|
@click.option("--output-dir", type=click.Path(file_okay=False, path_type=Path),
|
|
91
108
|
default=Path("./portacode-browser-artifacts"), show_default=True)
|
|
92
109
|
@click.option("--record-video/--no-record-video", default=False)
|
|
93
|
-
@click.option("--width", default=
|
|
94
|
-
@click.option("--height", default=
|
|
110
|
+
@click.option("--width", default=None, type=click.IntRange(240, 1920), help="Override saved viewport width.")
|
|
111
|
+
@click.option("--height", default=None, type=click.IntRange(240, 1920), help="Override saved viewport height.")
|
|
95
112
|
@click.option("--timeout-ms", default=10000, type=click.IntRange(250, 60000))
|
|
96
113
|
def browser_run(url, script, script_file, output_dir, record_video, width, height, timeout_ms):
|
|
97
114
|
"""Execute SCRIPT with page, context, screenshot(), dom(), and emit()."""
|
|
@@ -104,3 +121,26 @@ def browser_run(url, script, script_file, output_dir, record_video, width, heigh
|
|
|
104
121
|
click.echo(json.dumps(result, indent=2, default=str))
|
|
105
122
|
if not result["ok"]:
|
|
106
123
|
raise click.exceptions.Exit(1)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@browser_group.command("config")
|
|
127
|
+
@click.option("--viewport-width", type=click.IntRange(240, 1920))
|
|
128
|
+
@click.option("--viewport-height", type=click.IntRange(240, 1920))
|
|
129
|
+
@click.option("--video-width", type=click.IntRange(240, 1920))
|
|
130
|
+
@click.option("--video-height", type=click.IntRange(240, 1920))
|
|
131
|
+
def browser_config(viewport_width, viewport_height, video_width, video_height):
|
|
132
|
+
"""Show or persist default browser viewport and recording dimensions."""
|
|
133
|
+
updates = {
|
|
134
|
+
"viewport": {
|
|
135
|
+
key: value for key, value in {
|
|
136
|
+
"width": viewport_width, "height": viewport_height,
|
|
137
|
+
}.items() if value is not None
|
|
138
|
+
},
|
|
139
|
+
"video": {
|
|
140
|
+
key: value for key, value in {
|
|
141
|
+
"width": video_width, "height": video_height,
|
|
142
|
+
}.items() if value is not None
|
|
143
|
+
},
|
|
144
|
+
}
|
|
145
|
+
config = update_browser_config(updates) if any(updates.values()) else load_browser_config()
|
|
146
|
+
click.echo(json.dumps(config, indent=2))
|
|
@@ -16,7 +16,7 @@ from .terminal_handlers import (
|
|
|
16
16
|
)
|
|
17
17
|
from .git_handlers import GitActionHandler
|
|
18
18
|
from .system_handlers import SystemInfoHandler
|
|
19
|
-
from .browser_handlers import BrowserRunHandler
|
|
19
|
+
from .browser_handlers import BrowserRunHandler, BrowserConfigHandler
|
|
20
20
|
from .update_handler import UpdatePortacodeHandler
|
|
21
21
|
from .file_handlers import (
|
|
22
22
|
FileReadHandler,
|
|
@@ -103,6 +103,7 @@ __all__ = [
|
|
|
103
103
|
"GitActionHandler",
|
|
104
104
|
"SystemInfoHandler",
|
|
105
105
|
"BrowserRunHandler",
|
|
106
|
+
"BrowserConfigHandler",
|
|
106
107
|
"ConfigureProxmoxInfraHandler",
|
|
107
108
|
"CreateProxmoxContainerHandler",
|
|
108
109
|
"ResizeProxmoxContainerHandler",
|
|
@@ -9,6 +9,7 @@ import os
|
|
|
9
9
|
import re
|
|
10
10
|
import shutil
|
|
11
11
|
import sys
|
|
12
|
+
import tempfile
|
|
12
13
|
import time
|
|
13
14
|
from pathlib import Path
|
|
14
15
|
from typing import Any, Dict
|
|
@@ -25,7 +26,81 @@ _NODE_PATH: str | None = None
|
|
|
25
26
|
_NODE_PATH_LOCK: asyncio.Lock | None = None
|
|
26
27
|
_WORKER_IDLE_SECONDS = 300.0
|
|
27
28
|
_MAX_WORKER_RESPONSE_BYTES = 16 * 1024 * 1024
|
|
29
|
+
_BROWSER_CONFIG_NAME = "browser.json"
|
|
28
30
|
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
_DEFAULT_BROWSER_CONFIG = {
|
|
33
|
+
"viewport": {"width": 1280, "height": 720},
|
|
34
|
+
"video": {"width": 1280, "height": 720},
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _browser_config_path(message: dict | None = None) -> Path:
|
|
39
|
+
home = Path(get_runtime_user_home(message or {}))
|
|
40
|
+
return home / ".config" / "portacode" / _BROWSER_CONFIG_NAME
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _validated_dimension(section: str, key: str, value: Any) -> int:
|
|
44
|
+
try:
|
|
45
|
+
parsed = int(value)
|
|
46
|
+
except (TypeError, ValueError) as exc:
|
|
47
|
+
raise ValueError(f"{section}.{key} must be an integer") from exc
|
|
48
|
+
if not 240 <= parsed <= 1920:
|
|
49
|
+
raise ValueError(f"{section}.{key} must be between 240 and 1920")
|
|
50
|
+
return parsed
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def load_browser_config(message: dict | None = None) -> dict:
|
|
54
|
+
path = _browser_config_path(message)
|
|
55
|
+
config = json.loads(json.dumps(_DEFAULT_BROWSER_CONFIG))
|
|
56
|
+
try:
|
|
57
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
58
|
+
for section in ("viewport", "video"):
|
|
59
|
+
for key in ("width", "height"):
|
|
60
|
+
config[section][key] = _validated_dimension(
|
|
61
|
+
section, key, data.get(section, {}).get(key, config[section][key])
|
|
62
|
+
)
|
|
63
|
+
except (OSError, ValueError, TypeError, json.JSONDecodeError):
|
|
64
|
+
pass
|
|
65
|
+
return config
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def save_browser_config(config: dict, message: dict | None = None) -> dict:
|
|
69
|
+
path = _browser_config_path(message)
|
|
70
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
71
|
+
fd, temp = tempfile.mkstemp(prefix="browser-", dir=path.parent)
|
|
72
|
+
try:
|
|
73
|
+
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
|
74
|
+
json.dump(config, handle, indent=2)
|
|
75
|
+
handle.write("\n")
|
|
76
|
+
os.replace(temp, path)
|
|
77
|
+
finally:
|
|
78
|
+
try:
|
|
79
|
+
os.unlink(temp)
|
|
80
|
+
except FileNotFoundError:
|
|
81
|
+
pass
|
|
82
|
+
return config
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def update_browser_config(updates: dict, message: dict | None = None) -> dict:
|
|
86
|
+
if not isinstance(updates, dict):
|
|
87
|
+
raise ValueError("config must be an object")
|
|
88
|
+
unknown = set(updates) - {"viewport", "video"}
|
|
89
|
+
if unknown:
|
|
90
|
+
raise ValueError(f"unsupported browser config fields: {', '.join(sorted(unknown))}")
|
|
91
|
+
config = load_browser_config(message)
|
|
92
|
+
for section in ("viewport", "video"):
|
|
93
|
+
values = updates.get(section, {})
|
|
94
|
+
if not isinstance(values, dict):
|
|
95
|
+
raise ValueError(f"{section} must be an object")
|
|
96
|
+
unknown_dimensions = set(values) - {"width", "height"}
|
|
97
|
+
if unknown_dimensions:
|
|
98
|
+
raise ValueError(f"unsupported {section} fields: {', '.join(sorted(unknown_dimensions))}")
|
|
99
|
+
for key, value in values.items():
|
|
100
|
+
config[section][key] = _validated_dimension(section, key, value)
|
|
101
|
+
return save_browser_config(config, message)
|
|
102
|
+
|
|
103
|
+
|
|
29
104
|
_RUNNER = r"""
|
|
30
105
|
const fs = require('fs');
|
|
31
106
|
const readline = require('readline');
|
|
@@ -293,6 +368,19 @@ async def _worker_for(*, user: str, session_id: str, argv: list[str], env: dict[
|
|
|
293
368
|
return worker
|
|
294
369
|
|
|
295
370
|
|
|
371
|
+
class BrowserConfigHandler(AsyncHandler):
|
|
372
|
+
@property
|
|
373
|
+
def command_name(self) -> str:
|
|
374
|
+
return "browser_config"
|
|
375
|
+
|
|
376
|
+
async def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
377
|
+
config = load_browser_config(message)
|
|
378
|
+
updates = message.get("config")
|
|
379
|
+
if updates is not None:
|
|
380
|
+
config = update_browser_config(updates, message)
|
|
381
|
+
return {"event": "browser_config_response", "config": config}
|
|
382
|
+
|
|
383
|
+
|
|
296
384
|
class BrowserRunHandler(AsyncHandler):
|
|
297
385
|
@property
|
|
298
386
|
def command_name(self) -> str:
|
|
@@ -330,9 +418,14 @@ class BrowserRunHandler(AsyncHandler):
|
|
|
330
418
|
raise ValueError("browser action payload is too large")
|
|
331
419
|
if sum(1 for step in steps if isinstance(step, dict) and step.get("op") == "screenshot") > 4:
|
|
332
420
|
raise ValueError("at most four explicit screenshots are allowed per run")
|
|
421
|
+
configured = load_browser_config(message)
|
|
333
422
|
timeout_ms = min(max(int(message.get("timeout_ms") or 10_000), 250), 30_000)
|
|
334
|
-
|
|
335
|
-
|
|
423
|
+
viewport = {**configured["viewport"], **(message.get("viewport") or {})}
|
|
424
|
+
width = _validated_dimension("viewport", "width", viewport["width"])
|
|
425
|
+
height = _validated_dimension("viewport", "height", viewport["height"])
|
|
426
|
+
video = {**configured["video"], **(message.get("video") or {})}
|
|
427
|
+
video_width = _validated_dimension("video", "width", video["width"])
|
|
428
|
+
video_height = _validated_dimension("video", "height", video["height"])
|
|
336
429
|
user = get_default_runtime_user(message)
|
|
337
430
|
root = Path(get_runtime_user_home(message)) / ".local" / "share" / "portacode" / "browser"
|
|
338
431
|
profile = root / "profiles" / session_id
|
|
@@ -345,7 +438,7 @@ class BrowserRunHandler(AsyncHandler):
|
|
|
345
438
|
executable = "/usr/bin/chromium-browser" if Path("/usr/bin/chromium-browser").exists() else None
|
|
346
439
|
config = {
|
|
347
440
|
"session_id": session_id, "profile_dir": str(profile), "video_dir": str(video_dir) if video_dir else None,
|
|
348
|
-
"video_size": {"width":
|
|
441
|
+
"video_size": {"width": video_width, "height": video_height},
|
|
349
442
|
"viewport": {"width": width, "height": height}, "executable_path": executable,
|
|
350
443
|
"start_url": message.get("start_url"), "steps": steps, "timeout_ms": timeout_ms,
|
|
351
444
|
"deadline_ms": 45_000 if video_dir else None,
|
|
@@ -25,6 +25,7 @@ except ImportError: # pragma: no cover - py<3.8
|
|
|
25
25
|
|
|
26
26
|
from .base import SyncHandler
|
|
27
27
|
from .proxmox_infra import get_infra_snapshot
|
|
28
|
+
from .browser_handlers import load_browser_config
|
|
28
29
|
from .runtime_user import get_default_runtime_user, get_runtime_user_home
|
|
29
30
|
from portacode.tunneling.forwarding_state import load_forwarding_state
|
|
30
31
|
from portacode.connection.codex_app_server import CodexAppServer
|
|
@@ -630,6 +631,7 @@ class SystemInfoHandler(SyncHandler):
|
|
|
630
631
|
info["os_info"] = _get_os_info()
|
|
631
632
|
info["user_context"] = _get_user_context()
|
|
632
633
|
info["playwright"] = _get_playwright_info()
|
|
634
|
+
info["browser_config"] = load_browser_config()
|
|
633
635
|
info["proxmox"] = _get_proxmox_info()
|
|
634
636
|
info["cloudflare_tunnel"] = _get_cloudflare_tunnel_state()
|
|
635
637
|
info["cloudflare_forwarding"] = _get_cloudflare_forwarding_state()
|
|
@@ -2,11 +2,12 @@ import unittest
|
|
|
2
2
|
import asyncio
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
|
+
import tempfile
|
|
5
6
|
from types import SimpleNamespace
|
|
6
7
|
from unittest.mock import AsyncMock, Mock, patch
|
|
7
8
|
|
|
8
9
|
from . import browser_handlers
|
|
9
|
-
from .browser_handlers import BrowserRunHandler
|
|
10
|
+
from .browser_handlers import BrowserConfigHandler, BrowserRunHandler
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
class BrowserRunHandlerTests(unittest.IsolatedAsyncioTestCase):
|
|
@@ -215,3 +216,58 @@ class BrowserRunHandlerTests(unittest.IsolatedAsyncioTestCase):
|
|
|
215
216
|
def test_bounded_dom_operation_is_available(self):
|
|
216
217
|
self.assertIn("op==='dom'", browser_handlers._RUNNER)
|
|
217
218
|
self.assertIn("20000", browser_handlers._RUNNER)
|
|
219
|
+
|
|
220
|
+
async def test_persistent_mobile_config_controls_viewport_and_video(self):
|
|
221
|
+
worker = AsyncMock()
|
|
222
|
+
worker.run.return_value = {"ok": True, "steps": [], "screenshots": []}
|
|
223
|
+
with (
|
|
224
|
+
tempfile.TemporaryDirectory() as home,
|
|
225
|
+
patch.object(browser_handlers, "get_default_runtime_user", return_value="tester"),
|
|
226
|
+
patch.object(browser_handlers, "get_runtime_user_home", return_value=home),
|
|
227
|
+
patch.object(browser_handlers, "mkdir_with_owner"),
|
|
228
|
+
patch.object(browser_handlers, "wrap_argv_for_user", return_value=["node"]),
|
|
229
|
+
patch.object(browser_handlers, "_node_path", AsyncMock(return_value="/modules")),
|
|
230
|
+
patch.object(browser_handlers, "_node_executable", return_value="/bundled/node"),
|
|
231
|
+
patch.object(browser_handlers, "_playwright_js_package", return_value=None),
|
|
232
|
+
patch.object(browser_handlers, "_worker_for", AsyncMock(return_value=worker)),
|
|
233
|
+
):
|
|
234
|
+
configured = await BrowserConfigHandler(None, {}).execute({
|
|
235
|
+
"config": {
|
|
236
|
+
"viewport": {"width": 390, "height": 844},
|
|
237
|
+
"video": {"width": 390, "height": 844},
|
|
238
|
+
}
|
|
239
|
+
})
|
|
240
|
+
await self.handler.execute({
|
|
241
|
+
"session_id": "mobile", "steps": [], "record_video": True,
|
|
242
|
+
"final_screenshot": False,
|
|
243
|
+
})
|
|
244
|
+
saved = json.loads(
|
|
245
|
+
open(os.path.join(home, ".config", "portacode", "browser.json"), encoding="utf-8").read()
|
|
246
|
+
)
|
|
247
|
+
self.assertEqual(configured["config"], saved)
|
|
248
|
+
run_config = worker.run.await_args.args[0]
|
|
249
|
+
self.assertEqual(run_config["viewport"], {"width": 390, "height": 844})
|
|
250
|
+
self.assertEqual(run_config["video_size"], {"width": 390, "height": 844})
|
|
251
|
+
|
|
252
|
+
async def test_per_run_dimensions_override_saved_config(self):
|
|
253
|
+
worker = AsyncMock()
|
|
254
|
+
worker.run.return_value = {"ok": True, "steps": [], "screenshots": []}
|
|
255
|
+
saved = {"viewport": {"width": 390, "height": 844}, "video": {"width": 390, "height": 844}}
|
|
256
|
+
with (
|
|
257
|
+
patch.object(browser_handlers, "load_browser_config", return_value=saved),
|
|
258
|
+
patch.object(browser_handlers, "get_default_runtime_user", return_value="tester"),
|
|
259
|
+
patch.object(browser_handlers, "get_runtime_user_home", return_value="/tmp"),
|
|
260
|
+
patch.object(browser_handlers, "mkdir_with_owner"),
|
|
261
|
+
patch.object(browser_handlers, "wrap_argv_for_user", return_value=["node"]),
|
|
262
|
+
patch.object(browser_handlers, "_node_path", AsyncMock(return_value="/modules")),
|
|
263
|
+
patch.object(browser_handlers, "_node_executable", return_value="/bundled/node"),
|
|
264
|
+
patch.object(browser_handlers, "_playwright_js_package", return_value=None),
|
|
265
|
+
patch.object(browser_handlers, "_worker_for", AsyncMock(return_value=worker)),
|
|
266
|
+
):
|
|
267
|
+
await self.handler.execute({
|
|
268
|
+
"session_id": "override", "steps": [], "record_video": True,
|
|
269
|
+
"viewport": {"width": 430}, "video": {"height": 932},
|
|
270
|
+
})
|
|
271
|
+
run_config = worker.run.await_args.args[0]
|
|
272
|
+
self.assertEqual(run_config["viewport"], {"width": 430, "height": 844})
|
|
273
|
+
self.assertEqual(run_config["video_size"], {"width": 390, "height": 932})
|
portacode/connection/terminal.py
CHANGED
|
@@ -46,6 +46,7 @@ from .handlers import (
|
|
|
46
46
|
GitActionHandler,
|
|
47
47
|
SystemInfoHandler,
|
|
48
48
|
BrowserRunHandler,
|
|
49
|
+
BrowserConfigHandler,
|
|
49
50
|
FileReadHandler,
|
|
50
51
|
ImageReadHandler,
|
|
51
52
|
DirectoryListHandler,
|
|
@@ -575,6 +576,7 @@ class TerminalManager:
|
|
|
575
576
|
self._command_registry.register(GitActionHandler)
|
|
576
577
|
self._command_registry.register(SystemInfoHandler)
|
|
577
578
|
self._command_registry.register(BrowserRunHandler)
|
|
579
|
+
self._command_registry.register(BrowserConfigHandler)
|
|
578
580
|
# File operation handlers
|
|
579
581
|
self._command_registry.register(FileReadHandler)
|
|
580
582
|
self._command_registry.register(ImageReadHandler)
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import json
|
|
2
|
+
import os
|
|
2
3
|
from pathlib import Path
|
|
3
4
|
from unittest.mock import patch
|
|
4
5
|
|
|
5
6
|
from click.testing import CliRunner
|
|
6
7
|
|
|
7
8
|
from portacode.cli import cli
|
|
9
|
+
from portacode import browser_cli
|
|
8
10
|
|
|
9
11
|
|
|
10
12
|
def test_browser_run_accepts_inline_python_and_returns_json(tmp_path):
|
|
@@ -29,3 +31,21 @@ def test_browser_run_propagates_failure_exit_code(tmp_path):
|
|
|
29
31
|
with patch("portacode.browser_cli.run_browser_script", return_value={"ok": False, "error": "boom"}):
|
|
30
32
|
result = CliRunner().invoke(cli, ["browser", "run", "--script", "raise Exception()"])
|
|
31
33
|
assert result.exit_code == 1
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_managed_browser_cache_is_used_when_available(monkeypatch):
|
|
37
|
+
monkeypatch.delenv("PLAYWRIGHT_BROWSERS_PATH", raising=False)
|
|
38
|
+
monkeypatch.setattr(Path, "is_dir", lambda self: True)
|
|
39
|
+
|
|
40
|
+
browser_cli._configure_managed_browser_path()
|
|
41
|
+
|
|
42
|
+
assert os.environ["PLAYWRIGHT_BROWSERS_PATH"] == "/opt/ms-playwright"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_explicit_browser_cache_is_preserved(monkeypatch):
|
|
46
|
+
monkeypatch.setenv("PLAYWRIGHT_BROWSERS_PATH", "/custom/browsers")
|
|
47
|
+
monkeypatch.setattr(Path, "is_dir", lambda self: True)
|
|
48
|
+
|
|
49
|
+
browser_cli._configure_managed_browser_path()
|
|
50
|
+
|
|
51
|
+
assert os.environ["PLAYWRIGHT_BROWSERS_PATH"] == "/custom/browsers"
|
|
@@ -19,3 +19,18 @@ def test_system_info_includes_codex_installation_capability(monkeypatch):
|
|
|
19
19
|
"app_server_supported": True,
|
|
20
20
|
"ready": True,
|
|
21
21
|
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_system_info_includes_persistent_browser_config(monkeypatch):
|
|
25
|
+
expected = {
|
|
26
|
+
"viewport": {"width": 390, "height": 844},
|
|
27
|
+
"video": {"width": 390, "height": 844},
|
|
28
|
+
}
|
|
29
|
+
monkeypatch.setattr(
|
|
30
|
+
"portacode.connection.handlers.system_handlers.load_browser_config",
|
|
31
|
+
lambda: expected,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
payload = SystemInfoHandler(None, {}).execute({})
|
|
35
|
+
|
|
36
|
+
assert payload["info"]["browser_config"] == expected
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
portacode/README.md,sha256=R76fnJCKOnLiBEirTtc7fj4A6j5vAN1pdYMGNGqu1cs,1215
|
|
2
2
|
portacode/__init__.py,sha256=Hwk8wm5KA79qi06r0qKJKT2kjUlHrWaDtH4CyhnWFw8,1086
|
|
3
3
|
portacode/__main__.py,sha256=jmHTGC1hzmo9iKJLv-SSYe9BSIbPPZ2IOpecI03PlTs,296
|
|
4
|
-
portacode/_version.py,sha256=
|
|
5
|
-
portacode/browser_cli.py,sha256=
|
|
4
|
+
portacode/_version.py,sha256=H2sn0GWHtWK4XYFCqf5LLnQNzH5UrmtTH3ai6X37v24,535
|
|
5
|
+
portacode/browser_cli.py,sha256=XF__clsvyLsPLRVa4MezP6rijKBYsbHl-OLBJlV_63Y,6320
|
|
6
6
|
portacode/cli.py,sha256=k6wApK6PclUhyAmfOR91lHVA7zbA4IbcAsr0LSumYxk,40645
|
|
7
7
|
portacode/codex_loopback_proxy.py,sha256=RJ4oATShAf-m0RYlugIe6b6MzYrpb6aQ8f-xrx2AilI,24788
|
|
8
8
|
portacode/codex_prepare.py,sha256=VxIklS9cb9GF6fcFt7H_NN0AVXcnEzsGJmsaPliNXco,27595
|
|
@@ -34,17 +34,17 @@ portacode/connection/client.py,sha256=QDFRwBjATw5tp-wlhkiz5OfL_jVAwRHcVpPOYg6ntO
|
|
|
34
34
|
portacode/connection/codex_app_server.py,sha256=Q16bD78Hf14mtiWQ4QIB8eCTEY4wZrVytrXxr36Mo2E,23245
|
|
35
35
|
portacode/connection/guest_host_protocol.py,sha256=bXghGvUFuj2yg89pPljLuS4ivEYwgM3BwDK7CHIc2Dk,6199
|
|
36
36
|
portacode/connection/multiplex.py,sha256=L-TxqJ_ZEbfNEfu1cwxgJ5vUdyRzZjsMy2Kx1diiZys,5237
|
|
37
|
-
portacode/connection/terminal.py,sha256=
|
|
37
|
+
portacode/connection/terminal.py,sha256=pVlYPzOTk12phbSZYGgd-Hqgke9nAOH9ftmZdnsMxt0,74138
|
|
38
38
|
portacode/connection/test_guest_host_protocol.py,sha256=KhoXv4YMDYh6ENQrzW5rQNDZ3PMTP7oSN6iSC9KH4NE,2561
|
|
39
39
|
portacode/connection/test_guest_host_terminal.py,sha256=3c54_1fBHljdKmxpj4LYudVpgMZ-W8p_wB14WYVswps,5151
|
|
40
40
|
portacode/connection/test_webmin_proxy_config.py,sha256=52Qv9C0E5UzKUvQDJfREAb3RwJWs5R_CkKECrTWOsNg,1121
|
|
41
41
|
portacode/connection/webmin_proxy_config.py,sha256=vxzucOUz_GgxDjZWcgNVj7ImNfbdpIRABWpG_ihcK6s,2569
|
|
42
42
|
portacode/connection/handlers/README.md,sha256=hDIp55rOje2Qb55K0Np6EYZUe0vp4ATT3c4ANolwyf4,12521
|
|
43
43
|
portacode/connection/handlers/WEBSOCKET_PROTOCOL.md,sha256=y6B8VfZzXp2q3JDTcHqhDf2UegyhGx_7wQJhCeM2Iok,129814
|
|
44
|
-
portacode/connection/handlers/__init__.py,sha256=
|
|
44
|
+
portacode/connection/handlers/__init__.py,sha256=Xq3bNJwmsIg9oG7O4Kgzc-fg8OKS6aagSwdUEoyTZGw,5218
|
|
45
45
|
portacode/connection/handlers/automation_v2_handlers.py,sha256=eQT6XHABfxDTtI2ibczA5IdV0cBddqPe1SElv56eh_M,48950
|
|
46
46
|
portacode/connection/handlers/base.py,sha256=tMsKJmnNhXA3a4YZA55a0WO2e8-4Uh4BsG9lWAr3Ris,10829
|
|
47
|
-
portacode/connection/handlers/browser_handlers.py,sha256=
|
|
47
|
+
portacode/connection/handlers/browser_handlers.py,sha256=iYnKjYX-5YZyNVo3BMTkN2X52-0BJXy2xCf8l391w7I,21090
|
|
48
48
|
portacode/connection/handlers/chunked_content.py,sha256=h6hXRmxSeOgnIxoU8CkmvEf2Odv-ajPrpHIe_W3GKcA,9251
|
|
49
49
|
portacode/connection/handlers/cloudflare_forwarding.py,sha256=NXDKR9PZTngQ0AaH96QLICXPTKZMy-h6RCwa7F7nhVk,46928
|
|
50
50
|
portacode/connection/handlers/cloudflare_tunnel.py,sha256=qHBrm2rATxFmvfoC49YOBj0EUXjh_FETiWw66krxCIg,6655
|
|
@@ -60,10 +60,10 @@ portacode/connection/handlers/registry.py,sha256=dMv0fiCY4RFtkkyB1vmQMrrpD7zEhiP
|
|
|
60
60
|
portacode/connection/handlers/resumable_transfer_handlers.py,sha256=p8PaRRYh45LjKdfhIdHdUqREHpTlJ90yXWxnhAdYGaE,32671
|
|
61
61
|
portacode/connection/handlers/runtime_user.py,sha256=tvetxe4CKe8QzmROz9HE-dEpTlKJhg_7UjChyviOlOc,7707
|
|
62
62
|
portacode/connection/handlers/session.py,sha256=xUmSorFxKoLz7y9BLxfFD4VkSHEtL0bi_qf3mP0kmcw,46784
|
|
63
|
-
portacode/connection/handlers/system_handlers.py,sha256
|
|
63
|
+
portacode/connection/handlers/system_handlers.py,sha256=-BOmH3lfhvxC8hDkiSYHbcvF1D7j2T63bAruOm7LIo8,23775
|
|
64
64
|
portacode/connection/handlers/tab_factory.py,sha256=yn93h6GASjD1VpvW1oqpax3EpoT0r7r97zFXxML1wdA,16173
|
|
65
65
|
portacode/connection/handlers/terminal_handlers.py,sha256=s6bNmnXkjbV7rjSoYVDbbB9uHLGj2yNCz30wM6aRaHA,18651
|
|
66
|
-
portacode/connection/handlers/test_browser_handlers.py,sha256=
|
|
66
|
+
portacode/connection/handlers/test_browser_handlers.py,sha256=MtL1xc8phHPay1u-EdUd2KkaO2FU_8MYaZ2nkLVRWTE,13269
|
|
67
67
|
portacode/connection/handlers/test_cloudflare_forwarding.py,sha256=0rryq_r-VhTgi_spmCymnaJeLVVemsVF2qEIgXKz_Bc,11633
|
|
68
68
|
portacode/connection/handlers/test_file_move_copy.py,sha256=uXejCCcPWKs9qfyL6ckp0NxjwGYK3-Y63i3lFz-F47M,2872
|
|
69
69
|
portacode/connection/handlers/test_git_handlers.py,sha256=T9gFkCjanEC2L8n_kS2Ht0e67he34vThQtJU7j68auo,3514
|
|
@@ -104,7 +104,7 @@ portacode/link_capture/bin/__pycache__/link_capture_wrapper.cpython-38.pyc,sha25
|
|
|
104
104
|
portacode/static/js/test-ntp-clock.html,sha256=bUow9sifIuLNPqKvuPbpQozmEE6RhdCI4Plib3CqUmw,2130
|
|
105
105
|
portacode/static/js/utils/ntp-clock.js,sha256=t9moJyAGGU054BVtuGuSETVstlj1AoxVy44i4y9QWSs,6940
|
|
106
106
|
portacode/tests/test_automation_v2_inputs.py,sha256=iySvkskbqxFm7hSz19zNDwu8wGHvJkvHE-IQ3PKc42Q,2103
|
|
107
|
-
portacode/tests/test_browser_cli.py,sha256=
|
|
107
|
+
portacode/tests/test_browser_cli.py,sha256=M0L4v1oRe3cD40boZqGx_gTvytAPk9nLZlCy8e4fLbo,1902
|
|
108
108
|
portacode/tests/test_cli_service_logs.py,sha256=Fog68tVDMEe8zOcpfA1ukPRodhgjOwbTQ6Rm4leH6dQ,1733
|
|
109
109
|
portacode/tests/test_codex_app_server.py,sha256=xUiCno_Gx3Z5KrrhoxN38c5nGJBDgJ9bT7TDvFGZDMs,13993
|
|
110
110
|
portacode/tests/test_codex_handlers.py,sha256=vmwpifku1Dk7TI89H2z8nZQHueHa669NXxUbVghsreA,32083
|
|
@@ -114,7 +114,7 @@ portacode/tests/test_github_credential.py,sha256=k8n8tf26GqMjw3N_nSKztBQAv4ykQBe
|
|
|
114
114
|
portacode/tests/test_image_cli.py,sha256=By6fVtfFFu8IH7LYJqj0l3znZ30-KX-VIZ8H2RDwuKU,1646
|
|
115
115
|
portacode/tests/test_image_read_handler.py,sha256=jqSa7yBKSEvit-rqqsmig7I2--RSpsRzhgr7R1nCxfw,783
|
|
116
116
|
portacode/tests/test_resumable_transfer_handlers.py,sha256=Uc3vF7MMJjsTKqNO4_FOdgd0MVeEsfu8U2gV0dtt-iw,12171
|
|
117
|
-
portacode/tests/test_system_handlers.py,sha256=
|
|
117
|
+
portacode/tests/test_system_handlers.py,sha256=gbc5n6o3OxDpm-wRs4D2Y3IURfs1VesliGmRgLIMi_0,1119
|
|
118
118
|
portacode/tests/test_terminal_periodic_system_info.py,sha256=cYdTnI6lEigWGNtDH_54JWP9Mo4D6xr8MFHvtE_PADA,1940
|
|
119
119
|
portacode/tests/test_terminal_session_shell.py,sha256=nNM_fbT1vM8oknGDBeNk0eiRtZ8uz17dXXOB7STgGK8,2157
|
|
120
120
|
portacode/tunneling/__init__.py,sha256=qOrizn5awmPfsabVF59-EkmXnvS8AogvcBtVON6UfP0,54
|
|
@@ -132,7 +132,7 @@ portacode/utils/diff_apply.py,sha256=ArkT-vEQ7iyA1Nfqufq1AiNtRLhoCMnC75FCRh2kNgI
|
|
|
132
132
|
portacode/utils/diff_renderer.py,sha256=S76StnQ2DLfsz4Gg0m07UwPfRp8270PuzbNaQq-rmYk,13850
|
|
133
133
|
portacode/utils/ntp_clock.py,sha256=VqCnWCTehCufE43W23oB-WUdAZGeCcLxkmIOPwInYHc,2499
|
|
134
134
|
portacode/utils/runtime_paths.py,sha256=G9ROho5f5gnDhoQ7vpEAX2XifBzoJTvqTylkF2Hkx8E,1660
|
|
135
|
-
portacode-2.2.20.
|
|
135
|
+
portacode-2.2.20.dev4.dist-info/licenses/LICENSE,sha256=iiV44E3po4OL77NL8diCk3WtYvCNGu6gUci2TYxVUgQ,5520
|
|
136
136
|
test_modules/README.md,sha256=Do_agkm9WhSzueXjRAkV_xEj6Emy5zB3N3VKY5Roce8,9274
|
|
137
137
|
test_modules/__init__.py,sha256=1LcbHodIHsB0g-g4NGjSn6AMuCoGbymvXPYLOb6Z7F0,53
|
|
138
138
|
test_modules/test_device_online.py,sha256=QtYq0Dq9vME8Gp2O4fGSheqVf8LUtpsSKosXXk56gGM,1654
|
|
@@ -158,8 +158,8 @@ testing_framework/core/playwright_manager.py,sha256=asMe7zQg8Kp3a71F7pv92OiFt63B
|
|
|
158
158
|
testing_framework/core/runner.py,sha256=t3fq0mGCVUCE_TbieEPwmLKjxe44ez29SzW8G33ACME,19746
|
|
159
159
|
testing_framework/core/shared_cli_manager.py,sha256=BESSNtyQb7BOlaOvZmm04T8Uezjms4KCBs2MzTxvzYQ,8790
|
|
160
160
|
testing_framework/core/test_discovery.py,sha256=2FZ9fJ8Dp5dloA-fkgXoJ_gCMC_nYPBnA3Hs2xlagzM,4928
|
|
161
|
-
portacode-2.2.20.
|
|
162
|
-
portacode-2.2.20.
|
|
163
|
-
portacode-2.2.20.
|
|
164
|
-
portacode-2.2.20.
|
|
165
|
-
portacode-2.2.20.
|
|
161
|
+
portacode-2.2.20.dev4.dist-info/METADATA,sha256=n9OUaHJtBXU9up4EMMHmKeJHX__JjwRXQ3p7pJ28uII,12851
|
|
162
|
+
portacode-2.2.20.dev4.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
163
|
+
portacode-2.2.20.dev4.dist-info/entry_points.txt,sha256=0cV_SuIztVnLWACUY5KMR8K8pWdAsLiaFbLXE5He0rk,151
|
|
164
|
+
portacode-2.2.20.dev4.dist-info/top_level.txt,sha256=TGhTYUxfW8SyVZc_zGgzjzc24gGT7nSw8Qf73liVRKM,41
|
|
165
|
+
portacode-2.2.20.dev4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|