portacode 0.3.22__py3-none-any.whl → 0.3.24__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 +16 -3
- portacode/connection/handlers/WEBSOCKET_PROTOCOL.md +188 -16
- portacode/connection/handlers/__init__.py +4 -0
- portacode/connection/handlers/base.py +9 -5
- portacode/connection/handlers/chunked_content.py +244 -0
- portacode/connection/handlers/file_handlers.py +68 -2
- portacode/connection/handlers/project_aware_file_handlers.py +143 -1
- portacode/connection/handlers/project_state/git_manager.py +326 -66
- portacode/connection/handlers/project_state/handlers.py +307 -31
- portacode/connection/handlers/project_state/manager.py +44 -1
- portacode/connection/handlers/project_state/models.py +7 -0
- portacode/connection/handlers/project_state/utils.py +17 -1
- portacode/connection/handlers/project_state_handlers.py +1 -0
- portacode/connection/handlers/tab_factory.py +60 -7
- portacode/connection/terminal.py +13 -7
- {portacode-0.3.22.dist-info → portacode-0.3.24.dist-info}/METADATA +14 -3
- {portacode-0.3.22.dist-info → portacode-0.3.24.dist-info}/RECORD +25 -24
- {portacode-0.3.22.dist-info → portacode-0.3.24.dist-info}/WHEEL +1 -1
- test_modules/test_git_status_ui.py +24 -66
- testing_framework/core/playwright_manager.py +23 -0
- testing_framework/core/runner.py +10 -2
- testing_framework/core/test_discovery.py +7 -3
- {portacode-0.3.22.dist-info → portacode-0.3.24.dist-info}/entry_points.txt +0 -0
- {portacode-0.3.22.dist-info → portacode-0.3.24.dist-info/licenses}/LICENSE +0 -0
- {portacode-0.3.22.dist-info → portacode-0.3.24.dist-info}/top_level.txt +0 -0
|
@@ -14,6 +14,8 @@ from pathlib import Path
|
|
|
14
14
|
from typing import Optional, Dict, Any
|
|
15
15
|
|
|
16
16
|
from .project_state_handlers import TabInfo
|
|
17
|
+
from .project_state.utils import generate_content_hash
|
|
18
|
+
from .file_handlers import cache_content
|
|
17
19
|
|
|
18
20
|
logger = logging.getLogger(__name__)
|
|
19
21
|
|
|
@@ -57,7 +59,7 @@ MEDIA_EXTENSIONS = {
|
|
|
57
59
|
'.ico', '.icns', '.cur', '.psd', '.ai', '.eps', '.raw', '.cr2', '.nef',
|
|
58
60
|
|
|
59
61
|
# Audio
|
|
60
|
-
'.mp3', '.wav', '.flac', '.aac', '.ogg', '.wma', '.m4a', '.opus', '.webm',
|
|
62
|
+
'.mp3', '.wav', '.flac', '.aac', '.ogg', '.oga', '.wma', '.m4a', '.opus', '.webm',
|
|
61
63
|
|
|
62
64
|
# Video
|
|
63
65
|
'.mp4', '.avi', '.mkv', '.mov', '.wmv', '.flv', '.webm', '.m4v', '.3gp',
|
|
@@ -140,7 +142,11 @@ class TabFactory:
|
|
|
140
142
|
# Determine how to handle the file
|
|
141
143
|
if extension in IGNORED_EXTENSIONS:
|
|
142
144
|
tab_info['metadata']['ignored'] = True
|
|
143
|
-
|
|
145
|
+
content = f"# Binary file not displayed\n# File: {file_path.name}\n# Size: {self._format_file_size(file_size)}"
|
|
146
|
+
tab_info['content'] = content
|
|
147
|
+
content_hash = generate_content_hash(content)
|
|
148
|
+
tab_info['content_hash'] = content_hash
|
|
149
|
+
cache_content(content_hash, content)
|
|
144
150
|
return TabInfo(**tab_info)
|
|
145
151
|
|
|
146
152
|
# Handle different file types
|
|
@@ -181,6 +187,12 @@ class TabFactory:
|
|
|
181
187
|
if diff_details:
|
|
182
188
|
metadata['diff_details'] = diff_details
|
|
183
189
|
|
|
190
|
+
# Cache diff content
|
|
191
|
+
original_hash = generate_content_hash(original_content)
|
|
192
|
+
modified_hash = generate_content_hash(modified_content)
|
|
193
|
+
cache_content(original_hash, original_content)
|
|
194
|
+
cache_content(modified_hash, modified_content)
|
|
195
|
+
|
|
184
196
|
return TabInfo(
|
|
185
197
|
tab_id=tab_id,
|
|
186
198
|
tab_type='diff',
|
|
@@ -189,6 +201,8 @@ class TabFactory:
|
|
|
189
201
|
content=None, # Diff tabs don't use regular content
|
|
190
202
|
original_content=original_content,
|
|
191
203
|
modified_content=modified_content,
|
|
204
|
+
original_content_hash=original_hash,
|
|
205
|
+
modified_content_hash=modified_hash,
|
|
192
206
|
is_dirty=False,
|
|
193
207
|
mime_type=None,
|
|
194
208
|
encoding='utf-8',
|
|
@@ -219,6 +233,12 @@ class TabFactory:
|
|
|
219
233
|
if diff_details:
|
|
220
234
|
metadata['diff_details'] = diff_details
|
|
221
235
|
|
|
236
|
+
# Cache diff content
|
|
237
|
+
original_hash = generate_content_hash(original_content)
|
|
238
|
+
modified_hash = generate_content_hash(modified_content)
|
|
239
|
+
cache_content(original_hash, original_content)
|
|
240
|
+
cache_content(modified_hash, modified_content)
|
|
241
|
+
|
|
222
242
|
return TabInfo(
|
|
223
243
|
tab_id=tab_id,
|
|
224
244
|
tab_type='diff',
|
|
@@ -227,6 +247,8 @@ class TabFactory:
|
|
|
227
247
|
content=None, # Diff tabs don't use regular content
|
|
228
248
|
original_content=original_content,
|
|
229
249
|
modified_content=modified_content,
|
|
250
|
+
original_content_hash=original_hash,
|
|
251
|
+
modified_content_hash=modified_hash,
|
|
230
252
|
is_dirty=False,
|
|
231
253
|
mime_type=None,
|
|
232
254
|
encoding='utf-8',
|
|
@@ -248,12 +270,17 @@ class TabFactory:
|
|
|
248
270
|
if tab_id is None:
|
|
249
271
|
tab_id = str(uuid.uuid4())
|
|
250
272
|
|
|
273
|
+
# Cache untitled content
|
|
274
|
+
content_hash = generate_content_hash(content)
|
|
275
|
+
cache_content(content_hash, content)
|
|
276
|
+
|
|
251
277
|
return TabInfo(
|
|
252
278
|
tab_id=tab_id,
|
|
253
279
|
tab_type='untitled',
|
|
254
280
|
title="Untitled",
|
|
255
281
|
file_path=None,
|
|
256
282
|
content=content,
|
|
283
|
+
content_hash=content_hash,
|
|
257
284
|
original_content=None,
|
|
258
285
|
modified_content=None,
|
|
259
286
|
is_dirty=bool(content), # Dirty if has initial content
|
|
@@ -265,7 +292,11 @@ class TabFactory:
|
|
|
265
292
|
async def _load_text_content(self, file_path: Path, tab_info: Dict[str, Any], file_size: int):
|
|
266
293
|
"""Load text content from file."""
|
|
267
294
|
if file_size > MAX_TEXT_FILE_SIZE:
|
|
268
|
-
|
|
295
|
+
content = f"# File too large to display\n# File: {file_path.name}\n# Size: {self._format_file_size(file_size)}\n# Maximum size for text files: {self._format_file_size(MAX_TEXT_FILE_SIZE)}"
|
|
296
|
+
tab_info['content'] = content
|
|
297
|
+
content_hash = generate_content_hash(content)
|
|
298
|
+
tab_info['content_hash'] = content_hash
|
|
299
|
+
cache_content(content_hash, content)
|
|
269
300
|
tab_info['metadata']['truncated'] = True
|
|
270
301
|
return
|
|
271
302
|
|
|
@@ -275,6 +306,9 @@ class TabFactory:
|
|
|
275
306
|
try:
|
|
276
307
|
content = file_path.read_text(encoding=encoding)
|
|
277
308
|
tab_info['content'] = content
|
|
309
|
+
content_hash = generate_content_hash(content)
|
|
310
|
+
tab_info['content_hash'] = content_hash
|
|
311
|
+
cache_content(content_hash, content)
|
|
278
312
|
tab_info['encoding'] = encoding
|
|
279
313
|
self.logger.debug(f"Successfully loaded {file_path} with {encoding} encoding")
|
|
280
314
|
return
|
|
@@ -287,14 +321,22 @@ class TabFactory:
|
|
|
287
321
|
|
|
288
322
|
except OSError as e:
|
|
289
323
|
self.logger.error(f"Error reading file {file_path}: {e}")
|
|
290
|
-
|
|
324
|
+
content = f"# Error reading file\n# {e}"
|
|
325
|
+
tab_info['content'] = content
|
|
326
|
+
content_hash = generate_content_hash(content)
|
|
327
|
+
tab_info['content_hash'] = content_hash
|
|
328
|
+
cache_content(content_hash, content)
|
|
291
329
|
tab_info['metadata']['error'] = str(e)
|
|
292
330
|
|
|
293
331
|
async def _load_media_content(self, file_path: Path, tab_info: Dict[str, Any],
|
|
294
332
|
file_size: int, mime_type: Optional[str]):
|
|
295
333
|
"""Load media content as base64."""
|
|
296
334
|
if file_size > MAX_BINARY_FILE_SIZE:
|
|
297
|
-
|
|
335
|
+
content = f"# Media file too large to display\n# File: {file_path.name}\n# Size: {self._format_file_size(file_size)}"
|
|
336
|
+
tab_info['content'] = content
|
|
337
|
+
content_hash = generate_content_hash(content)
|
|
338
|
+
tab_info['content_hash'] = content_hash
|
|
339
|
+
cache_content(content_hash, content)
|
|
298
340
|
tab_info['metadata']['too_large'] = True
|
|
299
341
|
return
|
|
300
342
|
|
|
@@ -313,6 +355,9 @@ class TabFactory:
|
|
|
313
355
|
base64_content = base64.b64encode(binary_content).decode('ascii')
|
|
314
356
|
|
|
315
357
|
tab_info['content'] = base64_content
|
|
358
|
+
content_hash = generate_content_hash(base64_content)
|
|
359
|
+
tab_info['content_hash'] = content_hash
|
|
360
|
+
cache_content(content_hash, base64_content)
|
|
316
361
|
tab_info['encoding'] = 'base64'
|
|
317
362
|
tab_info['metadata']['original_size'] = file_size
|
|
318
363
|
|
|
@@ -320,12 +365,20 @@ class TabFactory:
|
|
|
320
365
|
|
|
321
366
|
except OSError as e:
|
|
322
367
|
self.logger.error(f"Error reading media file {file_path}: {e}")
|
|
323
|
-
|
|
368
|
+
content = f"# Error loading media file\n# {e}"
|
|
369
|
+
tab_info['content'] = content
|
|
370
|
+
content_hash = generate_content_hash(content)
|
|
371
|
+
tab_info['content_hash'] = content_hash
|
|
372
|
+
cache_content(content_hash, content)
|
|
324
373
|
tab_info['metadata']['error'] = str(e)
|
|
325
374
|
|
|
326
375
|
async def _load_binary_content(self, file_path: Path, tab_info: Dict[str, Any], file_size: int):
|
|
327
376
|
"""Handle binary files that can't be displayed."""
|
|
328
|
-
|
|
377
|
+
content = f"# Binary file\n# File: {file_path.name}\n# Size: {self._format_file_size(file_size)}\n# Type: {tab_info.get('mime_type', 'Unknown')}\n\n# This file contains binary data and cannot be displayed as text."
|
|
378
|
+
tab_info['content'] = content
|
|
379
|
+
content_hash = generate_content_hash(content)
|
|
380
|
+
tab_info['content_hash'] = content_hash
|
|
381
|
+
cache_content(content_hash, content)
|
|
329
382
|
tab_info['metadata']['binary'] = True
|
|
330
383
|
self.logger.debug(f"Marked {file_path} as binary file")
|
|
331
384
|
|
portacode/connection/terminal.py
CHANGED
|
@@ -30,25 +30,28 @@ from .handlers import (
|
|
|
30
30
|
TerminalListHandler,
|
|
31
31
|
SystemInfoHandler,
|
|
32
32
|
FileReadHandler,
|
|
33
|
-
FileWriteHandler,
|
|
34
33
|
DirectoryListHandler,
|
|
35
34
|
FileInfoHandler,
|
|
36
35
|
FileDeleteHandler,
|
|
37
|
-
FileCreateHandler,
|
|
38
|
-
FolderCreateHandler,
|
|
39
36
|
FileRenameHandler,
|
|
37
|
+
ContentRequestHandler,
|
|
40
38
|
ProjectStateFolderExpandHandler,
|
|
41
39
|
ProjectStateFolderCollapseHandler,
|
|
42
40
|
ProjectStateFileOpenHandler,
|
|
43
41
|
ProjectStateTabCloseHandler,
|
|
44
42
|
ProjectStateSetActiveTabHandler,
|
|
45
43
|
ProjectStateDiffOpenHandler,
|
|
44
|
+
ProjectStateDiffContentHandler,
|
|
46
45
|
ProjectStateGitStageHandler,
|
|
47
46
|
ProjectStateGitUnstageHandler,
|
|
48
47
|
ProjectStateGitRevertHandler,
|
|
49
48
|
ProjectStateGitCommitHandler,
|
|
50
49
|
)
|
|
51
|
-
from .handlers.project_aware_file_handlers import
|
|
50
|
+
from .handlers.project_aware_file_handlers import (
|
|
51
|
+
ProjectAwareFileWriteHandler,
|
|
52
|
+
ProjectAwareFileCreateHandler,
|
|
53
|
+
ProjectAwareFolderCreateHandler,
|
|
54
|
+
)
|
|
52
55
|
from .handlers.session import SessionManager
|
|
53
56
|
|
|
54
57
|
logger = logging.getLogger(__name__)
|
|
@@ -398,6 +401,7 @@ class TerminalManager:
|
|
|
398
401
|
"session_manager": self._session_manager,
|
|
399
402
|
"client_session_manager": self._client_session_manager,
|
|
400
403
|
"mux": mux,
|
|
404
|
+
"use_content_caching": True, # Enable content caching optimization
|
|
401
405
|
"debug": self.debug,
|
|
402
406
|
}
|
|
403
407
|
|
|
@@ -428,13 +432,14 @@ class TerminalManager:
|
|
|
428
432
|
self._command_registry.register(SystemInfoHandler)
|
|
429
433
|
# File operation handlers
|
|
430
434
|
self._command_registry.register(FileReadHandler)
|
|
431
|
-
self._command_registry.register(
|
|
435
|
+
self._command_registry.register(ProjectAwareFileWriteHandler) # Use project-aware version
|
|
432
436
|
self._command_registry.register(DirectoryListHandler)
|
|
433
437
|
self._command_registry.register(FileInfoHandler)
|
|
434
438
|
self._command_registry.register(FileDeleteHandler)
|
|
435
|
-
self._command_registry.register(
|
|
436
|
-
self._command_registry.register(
|
|
439
|
+
self._command_registry.register(ProjectAwareFileCreateHandler) # Use project-aware version
|
|
440
|
+
self._command_registry.register(ProjectAwareFolderCreateHandler) # Use project-aware version
|
|
437
441
|
self._command_registry.register(FileRenameHandler)
|
|
442
|
+
self._command_registry.register(ContentRequestHandler)
|
|
438
443
|
# Project state handlers
|
|
439
444
|
self._command_registry.register(ProjectStateFolderExpandHandler)
|
|
440
445
|
self._command_registry.register(ProjectStateFolderCollapseHandler)
|
|
@@ -442,6 +447,7 @@ class TerminalManager:
|
|
|
442
447
|
self._command_registry.register(ProjectStateTabCloseHandler)
|
|
443
448
|
self._command_registry.register(ProjectStateSetActiveTabHandler)
|
|
444
449
|
self._command_registry.register(ProjectStateDiffOpenHandler)
|
|
450
|
+
self._command_registry.register(ProjectStateDiffContentHandler)
|
|
445
451
|
self._command_registry.register(ProjectStateGitStageHandler)
|
|
446
452
|
self._command_registry.register(ProjectStateGitUnstageHandler)
|
|
447
453
|
self._command_registry.register(ProjectStateGitRevertHandler)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: portacode
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.24
|
|
4
4
|
Summary: Portacode CLI client and SDK
|
|
5
5
|
Home-page: https://github.com/portacode/portacode
|
|
6
6
|
Author: Meena Erian
|
|
@@ -18,15 +18,26 @@ Requires-Dist: websockets>=12.0
|
|
|
18
18
|
Requires-Dist: pyperclip>=1.8
|
|
19
19
|
Requires-Dist: psutil>=5.9
|
|
20
20
|
Requires-Dist: pyte>=0.8
|
|
21
|
+
Requires-Dist: pywinpty>=2.0; platform_system == "Windows"
|
|
21
22
|
Requires-Dist: GitPython>=3.1.45
|
|
22
23
|
Requires-Dist: watchdog>=3.0
|
|
23
24
|
Requires-Dist: diff-match-patch>=20230430
|
|
24
25
|
Requires-Dist: Pygments>=2.14.0
|
|
25
|
-
Requires-Dist: pywinpty>=2.0; platform_system == "Windows"
|
|
26
26
|
Provides-Extra: dev
|
|
27
27
|
Requires-Dist: black; extra == "dev"
|
|
28
28
|
Requires-Dist: flake8; extra == "dev"
|
|
29
29
|
Requires-Dist: pytest; extra == "dev"
|
|
30
|
+
Dynamic: author
|
|
31
|
+
Dynamic: author-email
|
|
32
|
+
Dynamic: classifier
|
|
33
|
+
Dynamic: description
|
|
34
|
+
Dynamic: description-content-type
|
|
35
|
+
Dynamic: home-page
|
|
36
|
+
Dynamic: license-file
|
|
37
|
+
Dynamic: provides-extra
|
|
38
|
+
Dynamic: requires-dist
|
|
39
|
+
Dynamic: requires-python
|
|
40
|
+
Dynamic: summary
|
|
30
41
|
|
|
31
42
|
# Portacode
|
|
32
43
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
portacode/README.md,sha256=4dKtpvR8LNgZPVz37GmkQCMWIr_u25Ao63iW56s7Ke4,775
|
|
2
2
|
portacode/__init__.py,sha256=oB3sV1wXr-um-RXio73UG8E5Xx6cF2ZVJveqjNmC-vQ,1086
|
|
3
3
|
portacode/__main__.py,sha256=jmHTGC1hzmo9iKJLv-SSYe9BSIbPPZ2IOpecI03PlTs,296
|
|
4
|
-
portacode/_version.py,sha256
|
|
4
|
+
portacode/_version.py,sha256=-sGbc_e4_IvCzq_UpGJECDvP_BbU_Ba1ROhJvn2pYYU,706
|
|
5
5
|
portacode/cli.py,sha256=Fcz8aXhgKhoWR9UbtmkN843DVuoJZtCTqBF3K-neVSc,16347
|
|
6
6
|
portacode/data.py,sha256=5-s291bv8J354myaHm1Y7CQZTZyRzMU3TGe5U4hb-FA,1591
|
|
7
7
|
portacode/keypair.py,sha256=PAcOYqlVLOoZTPYi6LvLjfsY6BkrWbLOhSZLb8r5sHs,3635
|
|
@@ -11,32 +11,34 @@ portacode/connection/README.md,sha256=f9rbuIEKa7cTm9C98rCiBbEtbiIXQU11esGSNhSMiJ
|
|
|
11
11
|
portacode/connection/__init__.py,sha256=atqcVGkViIEd7pRa6cP2do07RJOM0UWpbnz5zXjGktU,250
|
|
12
12
|
portacode/connection/client.py,sha256=tEM4rqzCRxIG5WXqYAT7s65NlEX2Z1sW42GosctBHIA,8013
|
|
13
13
|
portacode/connection/multiplex.py,sha256=L-TxqJ_ZEbfNEfu1cwxgJ5vUdyRzZjsMy2Kx1diiZys,5237
|
|
14
|
-
portacode/connection/terminal.py,sha256=
|
|
14
|
+
portacode/connection/terminal.py,sha256=OfjOjybC2RRrj_a3Eq17qVFF9mD1GlMn7l_O-mIcvIs,41716
|
|
15
15
|
portacode/connection/handlers/README.md,sha256=HsLZG1QK1JNm67HsgL6WoDg9nxzKXxwkc5fJPFJdX5g,12169
|
|
16
|
-
portacode/connection/handlers/WEBSOCKET_PROTOCOL.md,sha256=
|
|
17
|
-
portacode/connection/handlers/__init__.py,sha256=
|
|
18
|
-
portacode/connection/handlers/base.py,sha256=
|
|
19
|
-
portacode/connection/handlers/
|
|
20
|
-
portacode/connection/handlers/
|
|
21
|
-
portacode/connection/handlers/
|
|
16
|
+
portacode/connection/handlers/WEBSOCKET_PROTOCOL.md,sha256=0KBoJzqemXbvpu8Ps7LitLwRYhJNtzcTJ5WAHMGgAuc,62509
|
|
17
|
+
portacode/connection/handlers/__init__.py,sha256=4nv3Z4TGYjWcauKPWsbL_FbrTXApI94V7j6oiU1Vv-o,2144
|
|
18
|
+
portacode/connection/handlers/base.py,sha256=C-H61CUHM2k431CG0usd7eEqklDj9pnuXHujBwhTugk,6666
|
|
19
|
+
portacode/connection/handlers/chunked_content.py,sha256=h6hXRmxSeOgnIxoU8CkmvEf2Odv-ajPrpHIe_W3GKcA,9251
|
|
20
|
+
portacode/connection/handlers/file_handlers.py,sha256=CGMooOrfGbKx-bHA8vr8lmPG-vHw1DJlVWf6TLpfMJU,15355
|
|
21
|
+
portacode/connection/handlers/project_aware_file_handlers.py,sha256=n0M2WmBNWPwzigdIkyZiAsePUQGXVqYSsDyOxm-Nsok,9253
|
|
22
|
+
portacode/connection/handlers/project_state_handlers.py,sha256=v6ZefGW9i7n1aZLq2jOGumJIjYb6aHlPI4m1jkYewm8,1686
|
|
22
23
|
portacode/connection/handlers/registry.py,sha256=ebi0vhR1XXSYU7mJXlQJ4MjBYaMygGYqX7ReK7vsZ7o,5558
|
|
23
24
|
portacode/connection/handlers/session.py,sha256=7fHC46YXe5Q3uGxYPGpwcBaU_0eBhH4Hg8fmNVZsVmQ,24528
|
|
24
25
|
portacode/connection/handlers/system_handlers.py,sha256=65V5ctT0dIBc-oWG91e62MbdvU0z6x6JCTQuIqCWmZ0,5242
|
|
25
|
-
portacode/connection/handlers/tab_factory.py,sha256=
|
|
26
|
+
portacode/connection/handlers/tab_factory.py,sha256=VBZnwtxgeNJCsfBzUjkFWAAGBdijvai4MS2dXnhFY8U,18000
|
|
26
27
|
portacode/connection/handlers/terminal_handlers.py,sha256=Yuo84zwKB5OiLuVtDLCQgMVrOS3T8ZOONxXpGnnougo,11019
|
|
27
28
|
portacode/connection/handlers/project_state/README.md,sha256=trdd4ig6ungmwH5SpbSLfyxbL-QgPlGNU-_XrMEiXtw,10114
|
|
28
29
|
portacode/connection/handlers/project_state/__init__.py,sha256=5ucIqk6Iclqg6bKkL8r_wVs5Tlt6B9J7yQH6yQUt7gc,2541
|
|
29
30
|
portacode/connection/handlers/project_state/file_system_watcher.py,sha256=w-93ioUZZKZxzPFr8djJnGhWjMVFVdDsmo0fVAukoKk,10150
|
|
30
|
-
portacode/connection/handlers/project_state/git_manager.py,sha256=
|
|
31
|
-
portacode/connection/handlers/project_state/handlers.py,sha256=
|
|
32
|
-
portacode/connection/handlers/project_state/manager.py,sha256=
|
|
33
|
-
portacode/connection/handlers/project_state/models.py,sha256=
|
|
34
|
-
portacode/connection/handlers/project_state/utils.py,sha256=
|
|
31
|
+
portacode/connection/handlers/project_state/git_manager.py,sha256=fA0RbWCblpJep13L4MdqnEP4sE1qWc7Y66vrIo_SWps,76575
|
|
32
|
+
portacode/connection/handlers/project_state/handlers.py,sha256=nkednSbCC-0n3ZtzesaWd9_NFfxNjS4lyVNnbsYs0Zk,37823
|
|
33
|
+
portacode/connection/handlers/project_state/manager.py,sha256=03mN0H9TqVa_ohD5U5-5ZywDGj0s8-y1IxGdb07dZn8,57636
|
|
34
|
+
portacode/connection/handlers/project_state/models.py,sha256=EZTKvxHKs8QlQUbzI0u2IqfzfRRXZixUIDBwTGCJATI,4313
|
|
35
|
+
portacode/connection/handlers/project_state/utils.py,sha256=LsbQr9TH9Bz30FqikmtTxco4PlB_n0kUIuPKQ6Fb_mo,1665
|
|
36
|
+
portacode-0.3.24.dist-info/licenses/LICENSE,sha256=2FGbCnUDgRYuQTkB1O1dUUpu5CVAjK1j4_p6ack9Z54,1066
|
|
35
37
|
test_modules/README.md,sha256=Do_agkm9WhSzueXjRAkV_xEj6Emy5zB3N3VKY5Roce8,9274
|
|
36
38
|
test_modules/__init__.py,sha256=1LcbHodIHsB0g-g4NGjSn6AMuCoGbymvXPYLOb6Z7F0,53
|
|
37
39
|
test_modules/test_device_online.py,sha256=yiSyVaMwKAugqIX_ZIxmLXiOlmA_8IRXiUp12YmpB98,1653
|
|
38
40
|
test_modules/test_file_operations.py,sha256=KXbh9t8Fah1jZp1pEPlU4_F06iJIJr2fR-yYc4RL6m8,38372
|
|
39
|
-
test_modules/test_git_status_ui.py,sha256=
|
|
41
|
+
test_modules/test_git_status_ui.py,sha256=A_qkt-0lFLwxdr7t6YQaM0HqUElDwlZi84mlngg11RA,18734
|
|
40
42
|
test_modules/test_login_flow.py,sha256=eGjoZQj345qYxNm1arvYVNzjJ-TutT81lSdI6_8A5GU,1685
|
|
41
43
|
test_modules/test_navigate_testing_folder.py,sha256=-1EXceUEwof_sYp5paMWUNT3mAv5aIpYJ65_vqFbZew,18233
|
|
42
44
|
test_modules/test_terminal_buffer_performance.py,sha256=YQeDDZVnsQD3ug6udKUZH3NR7PHGP75uZsLZJYya7jg,12183
|
|
@@ -52,13 +54,12 @@ testing_framework/core/__init__.py,sha256=8AJQgqSCa9WgwkQNH_wTsA3JmJ4d4FRCweI-io
|
|
|
52
54
|
testing_framework/core/base_test.py,sha256=0kKQDNCdAJyTQfJiMBzx9_2MMRrmaVfQF0cawhvian4,13149
|
|
53
55
|
testing_framework/core/cli_manager.py,sha256=LDH_tWn-CmO08U_rmBIPpN_O6HLaQKRjdnfKGrtqs8Y,6991
|
|
54
56
|
testing_framework/core/hierarchical_runner.py,sha256=tCeksh2cXbRspurSiE-mQM1M1BOPeY8mKFbjvaBTVHw,26401
|
|
55
|
-
testing_framework/core/playwright_manager.py,sha256=
|
|
56
|
-
testing_framework/core/runner.py,sha256=
|
|
57
|
+
testing_framework/core/playwright_manager.py,sha256=9kGXJtRpRNEhaSlV7XVXvx4UQSHSvsFdlSnu74PjrJQ,19296
|
|
58
|
+
testing_framework/core/runner.py,sha256=j2QwNJmAxVBmJvcbVS7DgPJUKPNzqfLmt_4NNdaKmZU,19297
|
|
57
59
|
testing_framework/core/shared_cli_manager.py,sha256=BESSNtyQb7BOlaOvZmm04T8Uezjms4KCBs2MzTxvzYQ,8790
|
|
58
|
-
testing_framework/core/test_discovery.py,sha256=
|
|
59
|
-
portacode-0.3.
|
|
60
|
-
portacode-0.3.
|
|
61
|
-
portacode-0.3.
|
|
62
|
-
portacode-0.3.
|
|
63
|
-
portacode-0.3.
|
|
64
|
-
portacode-0.3.22.dist-info/RECORD,,
|
|
60
|
+
testing_framework/core/test_discovery.py,sha256=2FZ9fJ8Dp5dloA-fkgXoJ_gCMC_nYPBnA3Hs2xlagzM,4928
|
|
61
|
+
portacode-0.3.24.dist-info/METADATA,sha256=SS67gt2k2J9c4RMudmfgmce73fY93nhqzDHdX7IDZN8,7173
|
|
62
|
+
portacode-0.3.24.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
63
|
+
portacode-0.3.24.dist-info/entry_points.txt,sha256=lLUUL-BM6_wwe44Xv0__5NQ1BnAz6jWjSMFvZdWW3zU,48
|
|
64
|
+
portacode-0.3.24.dist-info/top_level.txt,sha256=TGhTYUxfW8SyVZc_zGgzjzc24gGT7nSw8Qf73liVRKM,41
|
|
65
|
+
portacode-0.3.24.dist-info/RECORD,,
|
|
@@ -21,7 +21,7 @@ class GitStatusUITest(BaseTest):
|
|
|
21
21
|
description="Test git status expandable section in file explorer UI",
|
|
22
22
|
tags=["git", "ui", "file-explorer", "expandable"],
|
|
23
23
|
depends_on=["file_operations_test"],
|
|
24
|
-
start_url="/
|
|
24
|
+
start_url="/project/1d98e739-de00-4d65-a13b-c6c82173683f/"
|
|
25
25
|
)
|
|
26
26
|
|
|
27
27
|
|
|
@@ -31,71 +31,6 @@ class GitStatusUITest(BaseTest):
|
|
|
31
31
|
assert_that = self.assert_that()
|
|
32
32
|
stats = self.stats()
|
|
33
33
|
|
|
34
|
-
# Navigate to testing_folder project (self-contained navigation)
|
|
35
|
-
# The navigate_testing_folder_test dependency ensures the git repo exists,
|
|
36
|
-
# but we need to navigate to it ourselves since tests don't share UI state
|
|
37
|
-
device_card = page.locator(".device-card.online").filter(has_text="portacode streamer")
|
|
38
|
-
await device_card.wait_for()
|
|
39
|
-
|
|
40
|
-
editor_button = device_card.get_by_text("Editor")
|
|
41
|
-
await editor_button.wait_for()
|
|
42
|
-
await editor_button.click()
|
|
43
|
-
|
|
44
|
-
# Wait for the project selector modal and select testing_folder
|
|
45
|
-
await page.wait_for_selector("#projectSelectorModal.show", timeout=10000)
|
|
46
|
-
await self.playwright_manager.take_screenshot("project_modal_opened")
|
|
47
|
-
|
|
48
|
-
await page.wait_for_selector(".item-list .section-header", timeout=10000)
|
|
49
|
-
await self.playwright_manager.take_screenshot("project_list_loaded")
|
|
50
|
-
|
|
51
|
-
# Try to find the testing_folder project specifically
|
|
52
|
-
testing_folder_item = page.locator('.item.project').filter(has_text="testing_folder")
|
|
53
|
-
testing_folder_count = await testing_folder_item.count()
|
|
54
|
-
|
|
55
|
-
# List all available projects for debugging
|
|
56
|
-
all_project_items = page.locator('.item.project')
|
|
57
|
-
all_project_count = await all_project_items.count()
|
|
58
|
-
stats.record_stat("total_projects_available", all_project_count)
|
|
59
|
-
|
|
60
|
-
if testing_folder_count > 0:
|
|
61
|
-
await testing_folder_item.first.click()
|
|
62
|
-
await self.playwright_manager.take_screenshot("testing_folder_selected")
|
|
63
|
-
stats.record_stat("found_testing_folder", True)
|
|
64
|
-
else:
|
|
65
|
-
# Take screenshot of available projects for debugging
|
|
66
|
-
await self.playwright_manager.take_screenshot("available_projects_debug")
|
|
67
|
-
|
|
68
|
-
# Fallback approach: find any project that might contain git files
|
|
69
|
-
if all_project_count > 0:
|
|
70
|
-
await all_project_items.first.click()
|
|
71
|
-
await self.playwright_manager.take_screenshot("fallback_project_selected")
|
|
72
|
-
stats.record_stat("found_testing_folder", False)
|
|
73
|
-
stats.record_stat("used_fallback_project", True)
|
|
74
|
-
else:
|
|
75
|
-
await self.playwright_manager.take_screenshot("no_projects_found")
|
|
76
|
-
assert_that.is_true(False, "No projects found in modal - navigate_testing_folder_test may have failed to create the testing_folder")
|
|
77
|
-
return TestResult(self.name, False, "No projects available")
|
|
78
|
-
|
|
79
|
-
# Wait for modal to close and project to load
|
|
80
|
-
await page.wait_for_selector("#projectSelectorModal.show", state="hidden", timeout=10000)
|
|
81
|
-
await self.playwright_manager.take_screenshot("project_modal_closed")
|
|
82
|
-
|
|
83
|
-
# Wait longer for file explorer to load properly and show project files
|
|
84
|
-
await page.wait_for_timeout(8000) # Increased wait time
|
|
85
|
-
await self.playwright_manager.take_screenshot("after_longer_wait")
|
|
86
|
-
|
|
87
|
-
# Try to wait for specific elements that indicate the project loaded
|
|
88
|
-
try:
|
|
89
|
-
# Wait for file items to appear (this indicates project loaded successfully)
|
|
90
|
-
await page.wait_for_selector(".file-item", timeout=10000)
|
|
91
|
-
await self.playwright_manager.take_screenshot("files_appeared")
|
|
92
|
-
except:
|
|
93
|
-
# If no files appear, continue anyway - maybe it's an empty project
|
|
94
|
-
await self.playwright_manager.take_screenshot("no_files_appeared")
|
|
95
|
-
pass
|
|
96
|
-
|
|
97
|
-
# Take initial screenshot to see the state
|
|
98
|
-
await self.playwright_manager.take_screenshot("initial_file_explorer")
|
|
99
34
|
|
|
100
35
|
# Check if project loaded properly - look for files in explorer
|
|
101
36
|
file_items = page.locator(".file-item")
|
|
@@ -121,6 +56,29 @@ class GitStatusUITest(BaseTest):
|
|
|
121
56
|
|
|
122
57
|
# Continue even if file explorer appears empty - the git section might still work
|
|
123
58
|
# The title bar shows "testing_folder" which suggests project is loaded
|
|
59
|
+
|
|
60
|
+
# PHASE 0: Test git diff viewer from file explorer
|
|
61
|
+
diff_options = [
|
|
62
|
+
'.context-menu-item:has-text("View Staged Changes")',
|
|
63
|
+
'.context-menu-item:has-text("View Unstaged Changes")',
|
|
64
|
+
'.context-menu-item:has-text("View All Changes")',
|
|
65
|
+
]
|
|
66
|
+
new_file_item = page.locator('.file-item:has(.file-name:text("new_file1.py"))')
|
|
67
|
+
for option in diff_options:
|
|
68
|
+
await new_file_item.first.click(button='right')
|
|
69
|
+
await page.locator(option).hover()
|
|
70
|
+
await page.wait_for_timeout(500) # Wait for context menu to appear
|
|
71
|
+
await self.playwright_manager.take_screenshot(f"before_clicking_diff_option_{option.split(':')[1]}")
|
|
72
|
+
await page.locator(option).click()
|
|
73
|
+
await page.wait_for_timeout(1000)
|
|
74
|
+
|
|
75
|
+
staged_diff_tab = page.locator('.editor-tab:has-text("(head → staged)")')
|
|
76
|
+
unstaged_diff_tab = page.locator('.editor-tab:has-text("(staged → working)")')
|
|
77
|
+
all_diff_tab = page.locator('.editor-tab:has-text("(head → working)")')
|
|
78
|
+
|
|
79
|
+
assert_that.is_true(await staged_diff_tab.is_visible(), "Staged diff tab should be visible")
|
|
80
|
+
assert_that.is_true(await unstaged_diff_tab.is_visible(), "Unstaged diff tab should be visible")
|
|
81
|
+
assert_that.is_true(await all_diff_tab.is_visible(), "All changes diff tab should be visible")
|
|
124
82
|
|
|
125
83
|
# PHASE 1: Test git status section expansion/collapse
|
|
126
84
|
stats.start_timer("git_section_detection")
|
|
@@ -312,6 +312,29 @@ class PlaywrightManager:
|
|
|
312
312
|
except Exception as e:
|
|
313
313
|
self.logger.error(f"Failed to write actions log: {e}")
|
|
314
314
|
|
|
315
|
+
async def log_timeline_marker(self, phase: str, description: str = ""):
|
|
316
|
+
"""Log a timeline marker for better test debugging and trace correlation."""
|
|
317
|
+
timestamp = datetime.now().isoformat()
|
|
318
|
+
marker_details = {
|
|
319
|
+
"phase": phase,
|
|
320
|
+
"description": description,
|
|
321
|
+
"timestamp": timestamp
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
# Log to actions for timeline tracking
|
|
325
|
+
await self.log_action("TIMELINE_MARKER", marker_details)
|
|
326
|
+
|
|
327
|
+
# Also log to console for visibility in trace viewer
|
|
328
|
+
if self.page:
|
|
329
|
+
try:
|
|
330
|
+
# Inject a console log into the page that will show up in traces
|
|
331
|
+
script = f"""
|
|
332
|
+
console.log('🧪 TEST PHASE: {phase}' + ({repr(description)} ? ' - ' + {repr(description)} : ''));
|
|
333
|
+
"""
|
|
334
|
+
asyncio.create_task(self.page.evaluate(script))
|
|
335
|
+
except Exception as e:
|
|
336
|
+
self.logger.warning(f"Could not inject timeline marker into page: {e}")
|
|
337
|
+
|
|
315
338
|
def _handle_console_message(self, msg):
|
|
316
339
|
"""Handle console messages from the page."""
|
|
317
340
|
console_entry = {
|
testing_framework/core/runner.py
CHANGED
|
@@ -192,11 +192,19 @@ class TestRunner:
|
|
|
192
192
|
tb_lines = traceback.format_tb(exc_traceback)
|
|
193
193
|
user_code_line = None
|
|
194
194
|
|
|
195
|
-
for
|
|
196
|
-
|
|
195
|
+
# Look for the LAST occurrence in user test code (most specific failure point)
|
|
196
|
+
for line in reversed(tb_lines):
|
|
197
|
+
if 'test_modules/' in line and '.py' in line:
|
|
197
198
|
user_code_line = line.strip()
|
|
198
199
|
break
|
|
199
200
|
|
|
201
|
+
# If no test_modules line found, look for any line with async context
|
|
202
|
+
if not user_code_line:
|
|
203
|
+
for line in reversed(tb_lines):
|
|
204
|
+
if 'await' in line or 'async' in line:
|
|
205
|
+
user_code_line = line.strip()
|
|
206
|
+
break
|
|
207
|
+
|
|
200
208
|
# Create detailed error message
|
|
201
209
|
error_details = [f"Test execution failed: {str(e)}"]
|
|
202
210
|
|
|
@@ -16,7 +16,7 @@ class TestDiscovery:
|
|
|
16
16
|
def __init__(self, test_directories: Optional[List[str]] = None):
|
|
17
17
|
self.test_directories = test_directories or ["tests", "test_modules"]
|
|
18
18
|
self.logger = logging.getLogger("test_discovery")
|
|
19
|
-
self.logger.setLevel(logging.
|
|
19
|
+
self.logger.setLevel(logging.ERROR) # Show errors during discovery
|
|
20
20
|
self.discovered_tests: Dict[str, BaseTest] = {}
|
|
21
21
|
|
|
22
22
|
def discover_tests(self, base_path: str = ".") -> Dict[str, BaseTest]:
|
|
@@ -60,10 +60,14 @@ class TestDiscovery:
|
|
|
60
60
|
self.discovered_tests[test_instance.name] = test_instance
|
|
61
61
|
self.logger.debug(f"Discovered test: {test_instance.name}")
|
|
62
62
|
except Exception as e:
|
|
63
|
-
|
|
63
|
+
error_msg = f"Failed to instantiate test {name} in {file_path}: {e}"
|
|
64
|
+
self.logger.error(error_msg)
|
|
65
|
+
print(f"❌ {error_msg}") # Also print to console for immediate visibility
|
|
64
66
|
|
|
65
67
|
except Exception as e:
|
|
66
|
-
|
|
68
|
+
error_msg = f"Failed to load test file {file_path}: {e}"
|
|
69
|
+
self.logger.error(error_msg)
|
|
70
|
+
print(f"❌ {error_msg}") # Also print to console for immediate visibility
|
|
67
71
|
|
|
68
72
|
def get_tests_by_category(self, category: TestCategory) -> List[BaseTest]:
|
|
69
73
|
"""Get all tests belonging to a specific category."""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|