diff --git a/api/routes.py b/api/routes.py index ac0d143f..7252fed7 100644 --- a/api/routes.py +++ b/api/routes.py @@ -9407,6 +9407,7 @@ from api.workspace import ( EscapeAuthorizationExpiredError, list_dir, list_authorized_escape_dir, + serialize_workspace_entries_for_browser, dir_signature, list_workspace_suggestions, read_file_content, @@ -16636,7 +16637,7 @@ def _handle_list_dir(handler, parsed): return j( handler, { - "entries": entries, + "entries": serialize_workspace_entries_for_browser(entries), "signature": dir_signature(Path(workspace), rel_path, entries), "path": rel_path, }, @@ -16706,6 +16707,7 @@ def _handle_escape_list_dir(handler, parsed): rel_path = qs.get("path", ["."])[0] try: payload = list_authorized_escape_dir(Path(s.workspace), sid, token, rel_path) + payload["entries"] = serialize_workspace_entries_for_browser(payload.get("entries")) return j(handler, payload) except FileNotFoundError as exc: return bad(handler, _sanitize_error(exc), 404) diff --git a/api/workspace.py b/api/workspace.py index 8f03b490..5c0ba580 100644 --- a/api/workspace.py +++ b/api/workspace.py @@ -16,6 +16,7 @@ import secrets import shutil import stat import subprocess +import sys import concurrent.futures import threading import time @@ -1231,6 +1232,40 @@ def rename_anchored(root: Path, source: Path, dest: Path) -> None: os.close(src_parent_fd) +def _birthtime_ns(lst) -> int | None: + """Return creation time in ns, or None when the platform lacks birthtime.""" + value = getattr(lst, 'st_birthtime_ns', None) + if value is not None: + return value + value = getattr(lst, 'st_birthtime', None) + if value is not None: + return int(value * 1_000_000_000) + if sys.platform == 'win32': + return getattr(lst, 'st_ctime_ns', None) + return None + + +def _browser_timestamp_ns(value) -> str | None: + if value is None: + return None + try: + return str(int(value)) + except (TypeError, ValueError): + return None + + +def serialize_workspace_entries_for_browser(entries: list[dict] | None) -> list[dict]: + payload = [] + for entry in entries or []: + item = dict(entry or {}) + if 'mtime_ns' in item: + item['mtime_ns'] = _browser_timestamp_ns(item.get('mtime_ns')) + if 'birthtime_ns' in item: + item['birthtime_ns'] = _browser_timestamp_ns(item.get('birthtime_ns')) + payload.append(item) + return payload + + def list_dir(workspace: Path, rel: str='.'): target = safe_resolve_ws(workspace, rel) if not target.is_dir(): @@ -1295,6 +1330,7 @@ def list_dir(workspace: Path, rel: str='.'): 'is_dir': False, 'target_outside_workspace': True, 'mtime_ns': mtime_ns, + 'birthtime_ns': _birthtime_ns(lstat_result) if lstat_result is not None else None, } entries.append(entry) else: @@ -1307,6 +1343,7 @@ def list_dir(workspace: Path, rel: str='.'): 'is_dir': is_dir, 'target_outside_workspace': False, 'mtime_ns': mtime_ns, + 'birthtime_ns': _birthtime_ns(lstat_result) if lstat_result is not None else None, } if not is_dir: try: @@ -1333,6 +1370,7 @@ def list_dir(workspace: Path, rel: str='.'): 'type': 'dir' if is_dir_entry else 'file', 'size': size, 'mtime_ns': mtime_ns, + 'birthtime_ns': _birthtime_ns(lstat_result) if lstat_result is not None else None, }) if _DIR_FD_OK: diff --git a/tests/test_issue6066_workspace_sort_api_payload.py b/tests/test_issue6066_workspace_sort_api_payload.py new file mode 100644 index 00000000..e7e06894 --- /dev/null +++ b/tests/test_issue6066_workspace_sort_api_payload.py @@ -0,0 +1,31 @@ +"""Route-facing regression coverage for exact workspace timestamp transport.""" + +from api import workspace as workspace_api + + +def test_browser_workspace_entries_stringify_nanosecond_fields(): + entries = [ + { + "name": "newer.txt", + "path": "newer.txt", + "type": "file", + "mtime_ns": 1_752_598_800_000_000_001, + "birthtime_ns": 1_752_598_800_000_000_000, + }, + { + "name": "missing.txt", + "path": "missing.txt", + "type": "file", + "mtime_ns": None, + "birthtime_ns": None, + }, + ] + + payload = workspace_api.serialize_workspace_entries_for_browser(entries) + + assert payload[0]["mtime_ns"] == "1752598800000000001" + assert payload[0]["birthtime_ns"] == "1752598800000000000" + assert payload[1]["mtime_ns"] is None + assert payload[1]["birthtime_ns"] is None + assert entries[0]["mtime_ns"] == 1_752_598_800_000_000_001 + assert entries[0]["birthtime_ns"] == 1_752_598_800_000_000_000