webpilot-engine 2.0.6__tar.gz → 2.0.7__tar.gz

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.
Files changed (78) hide show
  1. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/PKG-INFO +1 -1
  2. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/cli.py +18 -5
  3. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/pyproject.toml +1 -1
  4. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/supervisor/client.py +7 -2
  5. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/supervisor/contracts.py +1 -0
  6. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/supervisor/master_daemon.py +34 -40
  7. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/supervisor/shell.py +3 -3
  8. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/supervisor/worker_process.py +16 -3
  9. webpilot_engine-2.0.7/tests/test_supervisor.py +179 -0
  10. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/tests/test_supervisor_security.py +19 -0
  11. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/webpilot_engine.egg-info/PKG-INFO +1 -1
  12. webpilot_engine-2.0.6/tests/test_supervisor.py +0 -83
  13. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/README.md +0 -0
  14. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/adapters/__init__.py +0 -0
  15. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/adapters/base.py +0 -0
  16. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/adapters/browser_adapter.py +0 -0
  17. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/adapters/dom_scripts.py +0 -0
  18. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/adapters/envelope_builder.py +0 -0
  19. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/adapters/live_session_manager.py +0 -0
  20. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/adapters/upload_adapter.py +0 -0
  21. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/builder.py +0 -0
  22. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/common/__init__.py +0 -0
  23. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/common/cookies.py +0 -0
  24. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/common/logging.py +0 -0
  25. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/common/process_manager.py +0 -0
  26. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/common/retry.py +0 -0
  27. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/common/secrets.py +0 -0
  28. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/common/text.py +0 -0
  29. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/config/__init__.py +0 -0
  30. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/config/device_profile.example.json +0 -0
  31. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/config/device_profile.json +0 -0
  32. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/config/settings.example.json +0 -0
  33. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/config/settings.json +0 -0
  34. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/config/settings.py +0 -0
  35. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/constants/__init__.py +0 -0
  36. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/constants/contracts.py +0 -0
  37. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/constants/selectors.py +0 -0
  38. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/constants/timeouts.py +0 -0
  39. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/core/__init__.py +0 -0
  40. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/core/exceptions.py +0 -0
  41. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/core/models.py +0 -0
  42. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/engine.py +0 -0
  43. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/exceptions.py +0 -0
  44. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/mcp_server.py +0 -0
  45. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/models.py +0 -0
  46. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/orchestration/__init__.py +0 -0
  47. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/orchestration/context.py +0 -0
  48. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/orchestration/flow_orchestrator.py +0 -0
  49. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/orchestration/session_coordinator.py +0 -0
  50. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/services/__init__.py +0 -0
  51. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/services/auth_navigation_service.py +0 -0
  52. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/services/field_interaction_service.py +0 -0
  53. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/services/form_filler_service.py +0 -0
  54. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/services/inspection_formatter_service.py +0 -0
  55. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/services/inspection_service.py +0 -0
  56. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/services/reactive_interaction_service.py +0 -0
  57. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/setup.cfg +0 -0
  58. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/supervisor/__init__.py +0 -0
  59. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/supervisor/security.py +0 -0
  60. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/tests/test_config_and_builder.py +0 -0
  61. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/tests/test_contracts.py +0 -0
  62. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/tests/test_engine_facade.py +0 -0
  63. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/tests/test_enterprise_integration.py +0 -0
  64. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/tests/test_live_session.py +0 -0
  65. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/tests/test_mcp_server.py +0 -0
  66. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/tests/test_orchestration.py +0 -0
  67. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/tests/test_process_manager.py +0 -0
  68. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/tests/test_redaction.py +0 -0
  69. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/tests/test_secrets_resolution.py +0 -0
  70. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/tests/test_services.py +0 -0
  71. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/validators/__init__.py +0 -0
  72. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/validators/form_validator.py +0 -0
  73. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/validators/payload_validator.py +0 -0
  74. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/webpilot_engine.egg-info/SOURCES.txt +0 -0
  75. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/webpilot_engine.egg-info/dependency_links.txt +0 -0
  76. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/webpilot_engine.egg-info/entry_points.txt +0 -0
  77. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/webpilot_engine.egg-info/requires.txt +0 -0
  78. {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/webpilot_engine.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: webpilot-engine
3
- Version: 2.0.6
3
+ Version: 2.0.7
4
4
  Summary: WebPilot: Enterprise Autonomous Browser Engine, Universal Form Inspector & AI Agent Web Runtime
5
5
  Author: Abdulkarim Salih, WebPilot Core Team
6
6
  License: MIT
@@ -40,6 +40,11 @@ def get_base_url(args: argparse.Namespace) -> str:
40
40
  return f"http://{host}:{port}"
41
41
 
42
42
 
43
+ def get_supervisor_client(args: argparse.Namespace) -> SupervisorClient:
44
+ token = getattr(args, "token", None)
45
+ return SupervisorClient(base_url=get_base_url(args), token=token)
46
+
47
+
43
48
  def inspect_cmd(args: argparse.Namespace) -> None:
44
49
  """Executes the universal inspection workflow."""
45
50
  if getattr(args, "standalone", False):
@@ -63,7 +68,7 @@ def inspect_cmd(args: argparse.Namespace) -> None:
63
68
  return
64
69
 
65
70
  # Multi-tier Master Supervisor execution (default)
66
- client = SupervisorClient(base_url=get_base_url(args))
71
+ client = get_supervisor_client(args)
67
72
 
68
73
  if args.close_session:
69
74
  res = client.stop_service()
@@ -159,7 +164,7 @@ def apply_cmd(args: argparse.Namespace) -> None:
159
164
  return
160
165
 
161
166
  # Multi-tier Master Supervisor execution (default)
162
- client = SupervisorClient(base_url=get_base_url(args))
167
+ client = get_supervisor_client(args)
163
168
 
164
169
  if args.close_session:
165
170
  res = client.stop_service()
@@ -211,7 +216,7 @@ def apply_cmd(args: argparse.Namespace) -> None:
211
216
 
212
217
  def service_cmd(args: argparse.Namespace) -> None:
213
218
  """Manages the Master Supervisor daemon (status, restart, stop, start)."""
214
- client = SupervisorClient(base_url=get_base_url(args))
219
+ client = get_supervisor_client(args)
215
220
  action = args.service_action.lower()
216
221
 
217
222
  if action == "status":
@@ -259,7 +264,7 @@ def service_cmd(args: argparse.Namespace) -> None:
259
264
  def shell_cmd(args: argparse.Namespace) -> None:
260
265
  """Launches the interactive REPL shell."""
261
266
  from supervisor.shell import run_interactive_shell
262
- run_interactive_shell(host=args.host, port=args.port)
267
+ run_interactive_shell(host=args.host, port=args.port, token=getattr(args, "token", None))
263
268
 
264
269
 
265
270
  def main():
@@ -267,9 +272,12 @@ def main():
267
272
  from supervisor.master_daemon import run_master_daemon
268
273
  idx = sys.argv.index("--run-daemon")
269
274
  port = 9333
275
+ token = None
270
276
  if len(sys.argv) > idx + 1 and sys.argv[idx + 1].isdigit():
271
277
  port = int(sys.argv[idx + 1])
272
- run_master_daemon(port=port)
278
+ if len(sys.argv) > idx + 2:
279
+ token = sys.argv[idx + 2]
280
+ run_master_daemon(port=port, token=token)
273
281
  return
274
282
 
275
283
  if "--run-worker" in sys.argv:
@@ -286,6 +294,7 @@ def main():
286
294
  parser.add_argument("--env-file", help="Path to custom .env file")
287
295
  parser.add_argument("--host", default="127.0.0.1", help="Supervisor host address (default: 127.0.0.1)")
288
296
  parser.add_argument("--port", type=int, default=9333, help="Supervisor port (default: 9333)")
297
+ parser.add_argument("--token", help="Explicit Master Supervisor authentication token")
289
298
 
290
299
  subparsers = parser.add_subparsers(dest="command", required=True)
291
300
 
@@ -305,6 +314,7 @@ def main():
305
314
  p_inspect.add_argument("--standalone", action="store_true", help="Run in-process via FlowOrchestrator instead of Master Supervisor")
306
315
  p_inspect.add_argument("--host", default="127.0.0.1", help="Supervisor host address")
307
316
  p_inspect.add_argument("--port", type=int, default=9333, help="Supervisor port")
317
+ p_inspect.add_argument("--token", help="Explicit Master Supervisor authentication token")
308
318
  p_inspect.add_argument("--config", help="Path to custom settings.json configuration file")
309
319
  p_inspect.add_argument("--device-profile", help="Path to custom device_profile.json configuration file")
310
320
  p_inspect.add_argument("--env-file", help="Path to custom .env file")
@@ -329,6 +339,7 @@ def main():
329
339
  p_apply.add_argument("--standalone", action="store_true", help="Run in-process via FlowOrchestrator instead of Master Supervisor")
330
340
  p_apply.add_argument("--host", default="127.0.0.1", help="Supervisor host address")
331
341
  p_apply.add_argument("--port", type=int, default=9333, help="Supervisor port")
342
+ p_apply.add_argument("--token", help="Explicit Master Supervisor authentication token")
332
343
  p_apply.add_argument("--config", help="Path to custom settings.json configuration file")
333
344
  p_apply.add_argument("--device-profile", help="Path to custom device_profile.json configuration file")
334
345
  p_apply.add_argument("--env-file", help="Path to custom .env file")
@@ -337,12 +348,14 @@ def main():
337
348
  p_shell = subparsers.add_parser("shell", help="Launch interactive REPL automation shell")
338
349
  p_shell.add_argument("--host", default="127.0.0.1", help="Supervisor host address")
339
350
  p_shell.add_argument("--port", type=int, default=9333, help="Supervisor port")
351
+ p_shell.add_argument("--token", help="Explicit Master Supervisor authentication token")
340
352
 
341
353
  # 4. Supervisor Service Management Subcommand
342
354
  p_service = subparsers.add_parser("service", help="Manage Master Supervisor background daemon")
343
355
  p_service.add_argument("service_action", choices=["status", "restart", "stop", "start"], help="Action to execute")
344
356
  p_service.add_argument("--host", default="127.0.0.1", help="Supervisor host address")
345
357
  p_service.add_argument("--port", type=int, default=9333, help="Supervisor port")
358
+ p_service.add_argument("--token", help="Explicit Master Supervisor authentication token")
346
359
 
347
360
  # 5. Browser Binaries Installation Subcommand
348
361
  p_install = subparsers.add_parser("install", help="Download and configure required Playwright Chromium browser")
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "webpilot-engine"
7
- version = "2.0.6"
7
+ version = "2.0.7"
8
8
  description = "WebPilot: Enterprise Autonomous Browser Engine, Universal Form Inspector & AI Agent Web Runtime"
9
9
  readme = "README.md"
10
10
  authors = [
@@ -86,12 +86,17 @@ class SupervisorClient:
86
86
  cwd = PROJECT_ROOT
87
87
  cmd = [py_exe, script_path]
88
88
 
89
- get_process_manager().launch_detached(cmd, cwd=cwd)
89
+ spawn_env = os.environ.copy()
90
+ if self.token:
91
+ spawn_env["WEBPILOT_SUPERVISOR_TOKEN"] = self.token
92
+
93
+ get_process_manager().launch_detached(cmd, cwd=cwd, env=spawn_env)
90
94
 
91
95
  # Wait up to 4 seconds for supervisor to become active
92
96
  start = time.time()
93
97
  while time.time() - start < 4.0:
94
- self.token = load_supervisor_token()
98
+ if not self.token:
99
+ self.token = load_supervisor_token()
95
100
  if self.is_running():
96
101
  print("[✓] Master Supervisor is ready and active.")
97
102
  return
@@ -49,6 +49,7 @@ class SupervisorActionResponse:
49
49
  telemetry: Dict[str, Any] = field(default_factory=dict)
50
50
  schema: Optional[Dict[str, Any]] = None
51
51
  confirmed_fields: List[Any] = field(default_factory=list)
52
+ unconfirmed_fields: List[Any] = field(default_factory=list)
52
53
  failed_fields: List[Any] = field(default_factory=list)
53
54
  validation_errors: List[str] = field(default_factory=list)
54
55
  reactive_event: Optional[Dict[str, Any]] = None
@@ -88,33 +88,37 @@ class MasterSupervisor:
88
88
  self.watchdog_thread = threading.Thread(target=self._watchdog_loop, daemon=True)
89
89
  self.watchdog_thread.start()
90
90
 
91
+ def _spawn_worker(self) -> subprocess.Popen:
92
+ """Spawns Layer 2 Worker subprocess with process group isolation."""
93
+ if getattr(sys, "frozen", False):
94
+ worker_cmd = [sys.executable, "--run-worker"]
95
+ worker_cwd = os.path.dirname(sys.executable)
96
+ else:
97
+ worker_cmd = [sys.executable, "-m", "supervisor.worker_process"]
98
+ worker_cwd = PROJECT_ROOT
99
+
100
+ worker_env = os.environ.copy()
101
+ worker_env["WEBPILOT_SUPERVISOR_TOKEN"] = self.token
102
+
103
+ popen_kwargs: Dict[str, Any] = {
104
+ "stdin": subprocess.PIPE,
105
+ "stdout": subprocess.PIPE,
106
+ "stderr": subprocess.PIPE,
107
+ "text": True,
108
+ "bufsize": 1,
109
+ "cwd": worker_cwd,
110
+ "env": worker_env,
111
+ }
112
+ if sys.platform != "win32":
113
+ popen_kwargs["start_new_session"] = True
114
+
115
+ return subprocess.Popen(worker_cmd, **popen_kwargs)
116
+
91
117
  def _ensure_worker(self) -> subprocess.Popen:
92
118
  """Spawns Layer 2 Worker subprocess if not currently running."""
93
119
  with self.lock:
94
120
  if self.worker_process is None or self.worker_process.poll() is not None:
95
- if getattr(sys, "frozen", False):
96
- worker_cmd = [sys.executable, "--run-worker"]
97
- worker_cwd = os.path.dirname(sys.executable)
98
- else:
99
- worker_cmd = [sys.executable, "-m", "supervisor.worker_process"]
100
- worker_cwd = PROJECT_ROOT
101
-
102
- worker_env = os.environ.copy()
103
- worker_env["WEBPILOT_SUPERVISOR_TOKEN"] = self.token
104
-
105
- popen_kwargs: Dict[str, Any] = {
106
- "stdin": subprocess.PIPE,
107
- "stdout": subprocess.PIPE,
108
- "stderr": subprocess.PIPE,
109
- "text": True,
110
- "bufsize": 1,
111
- "cwd": worker_cwd,
112
- "env": worker_env,
113
- }
114
- if sys.platform != "win32":
115
- popen_kwargs["start_new_session"] = True
116
-
117
- self.worker_process = subprocess.Popen(worker_cmd, **popen_kwargs)
121
+ self.worker_process = self._spawn_worker()
118
122
  return self.worker_process
119
123
 
120
124
  def execute_action(self, req: SupervisorActionRequest) -> SupervisorActionResponse:
@@ -179,19 +183,8 @@ class MasterSupervisor:
179
183
  with self.lock:
180
184
  old_pid = self.worker_process.pid if self.worker_process else None
181
185
  self.terminate_worker()
182
- # Spawn fresh Worker
183
- worker_env = os.environ.copy()
184
- worker_env["WEBPILOT_SUPERVISOR_TOKEN"] = self.token
185
- new_worker = subprocess.Popen(
186
- [sys.executable, "-m", "supervisor.worker_process"],
187
- stdin=subprocess.PIPE,
188
- stdout=subprocess.PIPE,
189
- stderr=subprocess.PIPE,
190
- text=True,
191
- bufsize=1,
192
- cwd=PROJECT_ROOT,
193
- env=worker_env,
194
- )
186
+ # Spawn fresh Worker using unified helper with start_new_session on POSIX
187
+ new_worker = self._spawn_worker()
195
188
  self.worker_process = new_worker
196
189
  self.last_active_time = time.time()
197
190
 
@@ -306,13 +299,13 @@ def create_supervisor_handler(supervisor: MasterSupervisor):
306
299
  return SupervisorHTTPHandler
307
300
 
308
301
 
309
- def run_master_daemon(port: int = DEFAULT_SUPERVISOR_PORT):
302
+ def run_master_daemon(port: int = DEFAULT_SUPERVISOR_PORT, token: Optional[str] = None):
310
303
  """Starts the Master Supervisor HTTP service."""
311
304
  sys.stdout = LogWriter(LOG_FILE)
312
305
  sys.stderr = LogWriter(LOG_FILE)
313
306
  log_daemon(f"run_master_daemon starting on port {port} (PID: {os.getpid()})")
314
307
  try:
315
- supervisor = MasterSupervisor(port=port)
308
+ supervisor = MasterSupervisor(port=port, token=token)
316
309
  handler_class = create_supervisor_handler(supervisor)
317
310
  server = HTTPServer((DEFAULT_SUPERVISOR_HOST, port), handler_class)
318
311
  log_daemon(f"Master Supervisor listening on http://{DEFAULT_SUPERVISOR_HOST}:{port}")
@@ -332,6 +325,7 @@ def run_master_daemon(port: int = DEFAULT_SUPERVISOR_PORT):
332
325
 
333
326
 
334
327
  if __name__ == "__main__":
335
- port = int(sys.argv[1]) if len(sys.argv) > 1 else DEFAULT_SUPERVISOR_PORT
336
- run_master_daemon(port=port)
328
+ port = int(sys.argv[1]) if len(sys.argv) > 1 and sys.argv[1].isdigit() else DEFAULT_SUPERVISOR_PORT
329
+ token = sys.argv[2] if len(sys.argv) > 2 else None
330
+ run_master_daemon(port=port, token=token)
337
331
 
@@ -8,7 +8,7 @@ from __future__ import annotations
8
8
 
9
9
  import os
10
10
  import sys
11
- from typing import List
11
+ from typing import List, Optional
12
12
 
13
13
  PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
14
14
  if PROJECT_ROOT not in sys.path:
@@ -19,9 +19,9 @@ from supervisor.client import SupervisorClient
19
19
  from supervisor.contracts import SupervisorActionRequest, SupervisorActionResponse
20
20
 
21
21
 
22
- def run_interactive_shell(host: str = "127.0.0.1", port: int = 9333):
22
+ def run_interactive_shell(host: str = "127.0.0.1", port: int = 9333, token: Optional[str] = None):
23
23
  """Runs the interactive REPL session."""
24
- client = SupervisorClient(base_url=f"http://{host}:{port}")
24
+ client = SupervisorClient(base_url=f"http://{host}:{port}", token=token)
25
25
  try:
26
26
  client.ensure_running()
27
27
  except Exception as exc:
@@ -253,6 +253,7 @@ class OperationalWorker:
253
253
 
254
254
  field_svc = FieldInteractionService(page)
255
255
  confirmed = []
256
+ unconfirmed = []
256
257
  failed = []
257
258
 
258
259
  if fields_to_set:
@@ -260,12 +261,23 @@ class OperationalWorker:
260
261
  for key, val in fields_to_set:
261
262
  success = field_svc.set_field(key, val)
262
263
  if success:
263
- confirmed.append((key, val))
264
- lines.append(f" [✓] Set '{key}' -> '{val}'")
264
+ # Post-fill verification: read back DOM value to confirm it was applied
265
+ verified = field_svc.verify_field_value(key, val)
266
+ if not verified:
267
+ # Fallback retry via direct Playwright interaction
268
+ field_svc._playwright_click_fallback(key, val)
269
+ verified = field_svc.verify_field_value(key, val)
270
+
271
+ if verified:
272
+ confirmed.append((key, val))
273
+ lines.append(f" [✓] Set & Verified '{key}' -> '{val}'")
274
+ else:
275
+ unconfirmed.append((key, val))
276
+ lines.append(f" [?] Set but unconfirmed '{key}' -> '{val}'")
265
277
  else:
266
278
  failed.append((key, val))
267
279
  lines.append(f" [~] Unmatched/Failed '{key}' -> '{val}'")
268
- lines.append(f"[*] Fields summary: {len(confirmed)} set | {len(failed)} skipped.")
280
+ lines.append(f"[*] Fields summary: {len(confirmed)} confirmed | {len(unconfirmed)} unconfirmed | {len(failed)} skipped.")
269
281
 
270
282
  # 4. Handle uploads
271
283
  if req.upload_files:
@@ -349,6 +361,7 @@ class OperationalWorker:
349
361
  total_tabs=len(ctx.pages) if ctx else 0,
350
362
  tabs=coord.adapter.list_open_tabs(),
351
363
  confirmed_fields=confirmed,
364
+ unconfirmed_fields=unconfirmed,
352
365
  failed_fields=failed,
353
366
  validation_errors=validation_errors,
354
367
  schema=schema,
@@ -0,0 +1,179 @@
1
+ """Unit tests for Master Supervisor, Worker Process contracts, and Supervisor Client."""
2
+
3
+ import json
4
+ import pytest
5
+ from supervisor.contracts import SupervisorActionRequest, SupervisorActionResponse
6
+ from supervisor.client import SupervisorClient, DEFAULT_SUPERVISOR_URL
7
+
8
+
9
+ def test_supervisor_action_request_roundtrip():
10
+ """Verify SupervisorActionRequest serialization and deserialization."""
11
+ req = SupervisorActionRequest(
12
+ action="apply",
13
+ url="https://example.com/login",
14
+ fill_arguments=["username=testuser", "password=secret"],
15
+ press_buttons=["Sign In"],
16
+ submit=True,
17
+ screenshot_path="screenshot.png",
18
+ tab_index=1,
19
+ auto_inspect=False,
20
+ )
21
+ data = req.to_dict()
22
+ assert data["action"] == "apply"
23
+ assert data["url"] == "https://example.com/login"
24
+ assert data["fill_arguments"] == ["username=testuser", "password=secret"]
25
+ assert data["press_buttons"] == ["Sign In"]
26
+ assert data["submit"] is True
27
+ assert data["screenshot_path"] == "screenshot.png"
28
+ assert data["tab_index"] == 1
29
+ assert data["auto_inspect"] is False
30
+
31
+ reconstructed = SupervisorActionRequest.from_dict(data)
32
+ assert reconstructed == req
33
+
34
+
35
+ def test_supervisor_action_response_roundtrip():
36
+ """Verify SupervisorActionResponse serialization and deserialization."""
37
+ resp = SupervisorActionResponse(
38
+ success=True,
39
+ action="apply",
40
+ message="Success",
41
+ browser_pid=1234,
42
+ worker_pid=5678,
43
+ active_tab_index=0,
44
+ active_url="https://example.com/dashboard",
45
+ active_title="Dashboard",
46
+ total_tabs=1,
47
+ tabs=[{"index": 0, "title": "Dashboard", "url": "https://example.com/dashboard", "is_active": True}],
48
+ confirmed_fields=[("username", "testuser")],
49
+ failed_fields=[],
50
+ validation_errors=[],
51
+ output_lines=["[✓] Form submitted"],
52
+ )
53
+ data = resp.to_dict()
54
+ assert data["success"] is True
55
+ assert data["browser_pid"] == 1234
56
+ assert data["worker_pid"] == 5678
57
+ assert len(data["tabs"]) == 1
58
+ assert data["output_lines"] == ["[✓] Form submitted"]
59
+
60
+ reconstructed = SupervisorActionResponse.from_dict(data)
61
+ assert reconstructed == resp
62
+
63
+
64
+ def test_supervisor_client_default_url():
65
+ """Verify SupervisorClient initialization with default and custom URLs."""
66
+ client_default = SupervisorClient()
67
+ assert client_default.base_url == DEFAULT_SUPERVISOR_URL
68
+
69
+ client_custom = SupervisorClient(base_url="http://127.0.0.1:9555/")
70
+ assert client_custom.base_url == "http://127.0.0.1:9555"
71
+
72
+
73
+ def test_supervisor_action_request_defaults():
74
+ """Verify default values of SupervisorActionRequest."""
75
+ req = SupervisorActionRequest(action="inspect")
76
+ assert req.action == "inspect"
77
+ assert req.url is None
78
+ assert req.fill_arguments == []
79
+ assert req.press_buttons == []
80
+ assert req.upload_files == []
81
+ assert req.submit is False
82
+ assert req.auto_inspect is True
83
+ assert req.timeout_minutes == 30
84
+
85
+
86
+ def test_supervisor_spawn_and_restart_worker(monkeypatch):
87
+ """Verify MasterSupervisor._spawn_worker and restart_worker lifecycle with start_new_session."""
88
+ from supervisor.master_daemon import MasterSupervisor
89
+ import subprocess
90
+ import sys
91
+
92
+ sup = MasterSupervisor(port=9777, token="dummy-token")
93
+ sup._is_running = False # Stop watchdog thread
94
+
95
+ spawn_calls = []
96
+
97
+ class DummyProc:
98
+ def __init__(self, pid):
99
+ self.pid = pid
100
+ def poll(self):
101
+ return None
102
+
103
+ def fake_popen(cmd, **kwargs):
104
+ spawn_calls.append((cmd, kwargs))
105
+ return DummyProc(pid=1000 + len(spawn_calls))
106
+
107
+ monkeypatch.setattr(subprocess, "Popen", fake_popen)
108
+
109
+ # Test initial spawn via _ensure_worker
110
+ proc1 = sup._ensure_worker()
111
+ assert proc1.pid == 1001
112
+ assert len(spawn_calls) == 1
113
+ cmd1, kwargs1 = spawn_calls[0]
114
+ assert kwargs1["env"]["WEBPILOT_SUPERVISOR_TOKEN"] == "dummy-token"
115
+ if sys.platform != "win32":
116
+ assert kwargs1.get("start_new_session") is True
117
+
118
+ # Test restart_worker
119
+ terminated = []
120
+ monkeypatch.setattr(sup, "terminate_worker", lambda: terminated.append(True))
121
+ resp = sup.restart_worker()
122
+
123
+ assert resp.success is True
124
+ assert resp.action == "restart_worker"
125
+ assert resp.worker_pid == 1002
126
+ assert len(terminated) == 1
127
+ assert len(spawn_calls) == 2
128
+ cmd2, kwargs2 = spawn_calls[1]
129
+ # Verify that restart_worker uses the unified _spawn_worker
130
+ if sys.platform != "win32":
131
+ assert kwargs2.get("start_new_session") is True
132
+ assert kwargs2["env"]["WEBPILOT_SUPERVISOR_TOKEN"] == "dummy-token"
133
+
134
+
135
+ def test_worker_process_field_verification(monkeypatch):
136
+ """Verify worker_process calls verify_field_value and populates confirmed and unconfirmed fields."""
137
+ from supervisor.worker_process import OperationalWorker
138
+ from unittest.mock import MagicMock
139
+
140
+ worker = OperationalWorker()
141
+
142
+ mock_coord = MagicMock()
143
+ mock_coord.page.url = "https://example.com/form"
144
+ mock_coord.page.title.return_value = "Test Form"
145
+ mock_coord.page.is_closed.return_value = False
146
+ mock_coord.page.locator.return_value.count.return_value = 0
147
+ mock_coord.context.pages = [mock_coord.page]
148
+ worker.coordinator = mock_coord
149
+ monkeypatch.setattr(worker, "_ensure_session", lambda req: mock_coord)
150
+
151
+ # Mock FieldInteractionService
152
+ mock_field_svc = MagicMock()
153
+ mock_field_svc.set_field.side_effect = lambda k, v: k != "skipped_field"
154
+ mock_field_svc.verify_field_value.side_effect = lambda k, v: k == "verified_field"
155
+
156
+ monkeypatch.setattr("supervisor.worker_process.FieldInteractionService", lambda page: mock_field_svc)
157
+ monkeypatch.setattr("supervisor.worker_process.AuthNavigationService", lambda p, c: MagicMock())
158
+ monkeypatch.setattr("supervisor.worker_process.FormValidator", lambda p: MagicMock(get_validation_errors=lambda: []))
159
+ monkeypatch.setattr("supervisor.worker_process.InspectionService", lambda p: MagicMock())
160
+
161
+ req = SupervisorActionRequest(
162
+ action="apply",
163
+ url="https://example.com/form",
164
+ fill_arguments=[
165
+ "verified_field=value1",
166
+ "unverified_field=value2",
167
+ "skipped_field=value3",
168
+ ],
169
+ auto_inspect=False,
170
+ )
171
+
172
+ resp = worker._handle_apply(req)
173
+
174
+ assert resp.success is True
175
+ assert resp.confirmed_fields == [("verified_field", "value1")]
176
+ assert resp.unconfirmed_fields == [("unverified_field", "value2")]
177
+ assert resp.failed_fields == [("skipped_field", "value3")]
178
+ assert mock_field_svc.verify_field_value.called
179
+
@@ -87,3 +87,22 @@ def test_supervisor_http_auth_enforcement():
87
87
  finally:
88
88
  server.shutdown()
89
89
  server.server_close()
90
+
91
+
92
+ def test_cli_token_argument_and_client_propagation():
93
+ """Verify CLI --token flag parsing and propagation to SupervisorClient."""
94
+ import argparse
95
+ from cli import get_supervisor_client
96
+
97
+ # 1. Custom token provided in args
98
+ args_with_token = argparse.Namespace(host="127.0.0.1", port=9333, token="my-custom-token-xyz")
99
+ client = get_supervisor_client(args_with_token)
100
+ assert client.token == "my-custom-token-xyz"
101
+ headers = client._get_headers()
102
+ assert headers["X-Supervisor-Token"] == "my-custom-token-xyz"
103
+ assert headers["Authorization"] == "Bearer my-custom-token-xyz"
104
+
105
+ # 2. No token in args -> falls back to load_supervisor_token
106
+ args_no_token = argparse.Namespace(host="127.0.0.1", port=9333, token=None)
107
+ client_default = get_supervisor_client(args_no_token)
108
+ assert client_default.base_url == "http://127.0.0.1:9333"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: webpilot-engine
3
- Version: 2.0.6
3
+ Version: 2.0.7
4
4
  Summary: WebPilot: Enterprise Autonomous Browser Engine, Universal Form Inspector & AI Agent Web Runtime
5
5
  Author: Abdulkarim Salih, WebPilot Core Team
6
6
  License: MIT
@@ -1,83 +0,0 @@
1
- """Unit tests for Master Supervisor, Worker Process contracts, and Supervisor Client."""
2
-
3
- import json
4
- import pytest
5
- from supervisor.contracts import SupervisorActionRequest, SupervisorActionResponse
6
- from supervisor.client import SupervisorClient, DEFAULT_SUPERVISOR_URL
7
-
8
-
9
- def test_supervisor_action_request_roundtrip():
10
- """Verify SupervisorActionRequest serialization and deserialization."""
11
- req = SupervisorActionRequest(
12
- action="apply",
13
- url="https://example.com/login",
14
- fill_arguments=["username=testuser", "password=secret"],
15
- press_buttons=["Sign In"],
16
- submit=True,
17
- screenshot_path="screenshot.png",
18
- tab_index=1,
19
- auto_inspect=False,
20
- )
21
- data = req.to_dict()
22
- assert data["action"] == "apply"
23
- assert data["url"] == "https://example.com/login"
24
- assert data["fill_arguments"] == ["username=testuser", "password=secret"]
25
- assert data["press_buttons"] == ["Sign In"]
26
- assert data["submit"] is True
27
- assert data["screenshot_path"] == "screenshot.png"
28
- assert data["tab_index"] == 1
29
- assert data["auto_inspect"] is False
30
-
31
- reconstructed = SupervisorActionRequest.from_dict(data)
32
- assert reconstructed == req
33
-
34
-
35
- def test_supervisor_action_response_roundtrip():
36
- """Verify SupervisorActionResponse serialization and deserialization."""
37
- resp = SupervisorActionResponse(
38
- success=True,
39
- action="apply",
40
- message="Success",
41
- browser_pid=1234,
42
- worker_pid=5678,
43
- active_tab_index=0,
44
- active_url="https://example.com/dashboard",
45
- active_title="Dashboard",
46
- total_tabs=1,
47
- tabs=[{"index": 0, "title": "Dashboard", "url": "https://example.com/dashboard", "is_active": True}],
48
- confirmed_fields=[("username", "testuser")],
49
- failed_fields=[],
50
- validation_errors=[],
51
- output_lines=["[✓] Form submitted"],
52
- )
53
- data = resp.to_dict()
54
- assert data["success"] is True
55
- assert data["browser_pid"] == 1234
56
- assert data["worker_pid"] == 5678
57
- assert len(data["tabs"]) == 1
58
- assert data["output_lines"] == ["[✓] Form submitted"]
59
-
60
- reconstructed = SupervisorActionResponse.from_dict(data)
61
- assert reconstructed == resp
62
-
63
-
64
- def test_supervisor_client_default_url():
65
- """Verify SupervisorClient initialization with default and custom URLs."""
66
- client_default = SupervisorClient()
67
- assert client_default.base_url == DEFAULT_SUPERVISOR_URL
68
-
69
- client_custom = SupervisorClient(base_url="http://127.0.0.1:9555/")
70
- assert client_custom.base_url == "http://127.0.0.1:9555"
71
-
72
-
73
- def test_supervisor_action_request_defaults():
74
- """Verify default values of SupervisorActionRequest."""
75
- req = SupervisorActionRequest(action="inspect")
76
- assert req.action == "inspect"
77
- assert req.url is None
78
- assert req.fill_arguments == []
79
- assert req.press_buttons == []
80
- assert req.upload_files == []
81
- assert req.submit is False
82
- assert req.auto_inspect is True
83
- assert req.timeout_minutes == 30