fix(workspace): transport server partition rank for workspace sorts (#6066)

The frontend re-derived partition rank from the serialized type, so FIFO,
socket, and device entries moved into the regular-file partition on non-default
sorts. Emit workspace_sort_rank from list_dir, consume it in the rank helper,
and keep it out of dir_signature.
This commit is contained in:
Rod Boev committed 2026-08-01 18:46:37 -04:00
1 parent 573ac33182
commit 88c4b9a6b8
5 files changed
+64 -14

No files matched your search

+7
View File
@@ -1280,6 +1280,8 @@ def list_dir(workspace: Path, rel: str='.'):
with follow_symlinks=False (else None); ``reachable`` is False when a
follow_symlinks=True stat raised (broken target or symlink loop)."""
if is_symlink:
# Keep the transport rank aligned with _sort_key_de/_sort_key_p.
workspace_sort_rank = 0
if raw_link is None:
return
# A symlink whose follow-stat raised (ELOOP / broken target) can never
@@ -1328,6 +1330,7 @@ def list_dir(workspace: Path, rel: str='.'):
'path': display_path,
'type': 'symlink',
'is_dir': False,
'workspace_sort_rank': workspace_sort_rank,
'target_outside_workspace': True,
'mtime_ns': mtime_ns,
'birthtime_ns': _birthtime_ns(lstat_result) if lstat_result is not None else None,
@@ -1341,6 +1344,7 @@ def list_dir(workspace: Path, rel: str='.'):
'type': 'symlink',
'target': str(link_target),
'is_dir': is_dir,
'workspace_sort_rank': workspace_sort_rank,
'target_outside_workspace': False,
'mtime_ns': mtime_ns,
'birthtime_ns': _birthtime_ns(lstat_result) if lstat_result is not None else None,
@@ -1357,6 +1361,7 @@ def list_dir(workspace: Path, rel: str='.'):
entry_path = rel + '/' + name
if lstat_result is not None:
is_file = stat.S_ISREG(lstat_result.st_mode)
workspace_sort_rank = 2 if is_file else 1
size = lstat_result.st_size if is_file else None
mtime_ns = lstat_result.st_mtime_ns
is_dir_entry = stat.S_ISDIR(lstat_result.st_mode)
@@ -1364,6 +1369,7 @@ def list_dir(workspace: Path, rel: str='.'):
size = None
mtime_ns = None
is_dir_entry = False
workspace_sort_rank = 1
entries.append({
'name': name,
'path': entry_path,
@@ -1371,6 +1377,7 @@ def list_dir(workspace: Path, rel: str='.'):
'size': size,
'mtime_ns': mtime_ns,
'birthtime_ns': _birthtime_ns(lstat_result) if lstat_result is not None else None,
'workspace_sort_rank': workspace_sort_rank,
})
if _DIR_FD_OK:
+2 -4
View File
@@ -18759,10 +18759,8 @@ function _normalizeWorkspaceSortKey(value){
return WORKSPACE_SORT_KEYS.includes(value)?value:WORKSPACE_SORT_DEFAULT;
}
function _workspaceEntryRank(item){
const type=item&&item.type;
if(type==='symlink') return 0;
if(type==='dir') return 1;
return 2;
const rank=parseInt(item&&item.workspace_sort_rank,10);
return rank===0||rank===1||rank===2?rank:1;
}
function _workspaceEntryTimestampKey(item,field){
const raw=item&&item[field];
+43 -10
View File
@@ -2,6 +2,7 @@
import hashlib
import json
import os
import subprocess
from pathlib import Path
@@ -22,6 +23,31 @@ def test_list_dir_emits_birthtime_ns(tmp_path):
assert {entry["type"] for entry in entries} == {"file", "dir"}
assert all("birthtime_ns" in entry for entry in entries)
assert all(isinstance(entry["birthtime_ns"], (int, type(None))) for entry in entries)
assert {entry["workspace_sort_rank"] for entry in entries} == {1, 2}
def test_list_dir_emits_server_partition_rank_for_special_entries(tmp_path, monkeypatch):
if not hasattr(os, "mkfifo"):
pytest.skip("FIFO creation is unavailable on this platform")
try:
os.mkfifo(tmp_path / "fifo")
(tmp_path / "directory").mkdir()
(tmp_path / "file.txt").write_text("x", encoding="utf-8")
link_target = tmp_path / "file.txt"
(tmp_path / "link").symlink_to(link_target)
except (OSError, NotImplementedError) as exc:
pytest.skip(f"special entry creation unavailable: {exc}")
modes = [False]
if workspace_api._DIR_FD_OK:
modes.append(True)
for use_dir_fd in modes:
monkeypatch.setattr(workspace_api, "_DIR_FD_OK", use_dir_fd)
entries = {entry["name"]: entry for entry in workspace_api.list_dir(tmp_path, ".")}
assert entries["link"]["workspace_sort_rank"] == 0
assert entries["directory"]["workspace_sort_rank"] == 1
assert entries["fifo"]["workspace_sort_rank"] == 1
assert entries["file.txt"]["workspace_sort_rank"] == 2
def test_birthtime_ns_platform_matrix(monkeypatch):
@@ -105,7 +131,7 @@ function _visibleWorkspaceEntries(entries){return S.showHiddenWorkspaceFiles?ent
def test_sort_modified_desc_within_rank():
result = _run_sort_harness("""
S.workspaceSortKey='modified-desc';
const input=[{name:'file-old',type:'file',mtime_ns:1},{name:'dir',type:'dir',mtime_ns:2},{name:'file-new',type:'file',mtime_ns:3},{name:'link',type:'symlink',mtime_ns:0}];
const input=[{name:'file-old',type:'file',workspace_sort_rank:2,mtime_ns:1},{name:'dir',type:'dir',workspace_sort_rank:1,mtime_ns:2},{name:'file-new',type:'file',workspace_sort_rank:2,mtime_ns:3},{name:'link',type:'symlink',workspace_sort_rank:0,mtime_ns:0}];
const out=_workspaceEntriesForRender(input).map(x=>x.name);
if(JSON.stringify(out)!=='[\"link\",\"dir\",\"file-new\",\"file-old\"]')process.exit(1);
""")
@@ -115,7 +141,7 @@ if(JSON.stringify(out)!=='[\"link\",\"dir\",\"file-new\",\"file-old\"]')process.
def test_sort_name_desc_within_rank():
result = _run_sort_harness("""
S.workspaceSortKey='name-desc';
const input=[{name:'a',type:'file'},{name:'dir',type:'dir'},{name:'b',type:'file'},{name:'link',type:'symlink'}];
const input=[{name:'a',type:'file',workspace_sort_rank:2},{name:'dir',type:'dir',workspace_sort_rank:1},{name:'b',type:'file',workspace_sort_rank:2},{name:'link',type:'symlink',workspace_sort_rank:0}];
const out=_workspaceEntriesForRender(input).map(x=>x.name);
if(JSON.stringify(out)!=='[\"link\",\"dir\",\"b\",\"a\"]')process.exit(1);
""")
@@ -133,8 +159,15 @@ if(out!==input||out[0]!==input[0])process.exit(1);
def test_rank_grouping_holds_all_keys():
result = _run_sort_harness("""
const input=[{name:'s',type:'symlink',mtime_ns:1},{name:'d',type:'dir',mtime_ns:2},{name:'f',type:'file',mtime_ns:3}];
for(const key of WORKSPACE_SORT_KEYS){S.workspaceSortKey=key;const ranks=_workspaceEntriesForRender(input).map(_workspaceEntryRank);if(ranks.some((x,i)=>i&&x<ranks[i-1]))process.exit(1)}
const input=[{name:'file',type:'file',workspace_sort_rank:2,mtime_ns:3},{name:'fifo',type:'file',workspace_sort_rank:1,mtime_ns:4},{name:'dir',type:'dir',workspace_sort_rank:1,mtime_ns:2},{name:'link',type:'symlink',workspace_sort_rank:0,mtime_ns:1}];
for(const key of ['name-desc','created-desc','modified-desc']){S.workspaceSortKey=key;S._workspaceBirthtimeSeen=true;const ranks=_workspaceEntriesForRender(input).map(_workspaceEntryRank);if(JSON.stringify(ranks)!=='[0,1,1,2]')process.exit(1)}
""")
assert result.returncode == 0, result.stderr
def test_unknown_workspace_rank_uses_non_regular_partition():
result = _run_sort_harness("""
if(_workspaceEntryRank({type:'file'})!==1||_workspaceEntryRank({type:'file',workspace_sort_rank:'bogus'})!==1||_workspaceEntryRank({type:'file',workspace_sort_rank:3})!==1)process.exit(1);
""")
assert result.returncode == 0, result.stderr
@@ -142,7 +175,7 @@ for(const key of WORKSPACE_SORT_KEYS){S.workspaceSortKey=key;const ranks=_worksp
def test_missing_timestamps_sort_last():
result = _run_sort_harness("""
S.workspaceSortKey='created-desc';S._workspaceBirthtimeSeen=true;
const input=[{name:'missing',type:'file',birthtime_ns:null},{name:'zero',type:'file',birthtime_ns:0},{name:'new',type:'file',birthtime_ns:2},{name:'undefined',type:'file'}];
const input=[{name:'missing',type:'file',workspace_sort_rank:2,birthtime_ns:null},{name:'zero',type:'file',workspace_sort_rank:2,birthtime_ns:0},{name:'new',type:'file',workspace_sort_rank:2,birthtime_ns:2},{name:'undefined',type:'file',workspace_sort_rank:2}];
const out=_workspaceEntriesForRender(input).map(x=>x.name);
if(JSON.stringify(out)!=='[\"new\",\"zero\",\"missing\",\"undefined\"]')process.exit(1);
""")
@@ -152,7 +185,7 @@ if(JSON.stringify(out)!=='[\"new\",\"zero\",\"missing\",\"undefined\"]')process.
def test_large_nanosecond_strings_sort_exactly():
result = _run_sort_harness("""
S.workspaceSortKey='modified-desc';
const input=[{name:'older',type:'file',mtime_ns:'1752598800000000000'},{name:'newer',type:'file',mtime_ns:'1752598800000000001'}];
const input=[{name:'older',type:'file',workspace_sort_rank:2,mtime_ns:'1752598800000000000'},{name:'newer',type:'file',workspace_sort_rank:2,mtime_ns:'1752598800000000001'}];
const out=_workspaceEntriesForRender(input).map(x=>x.name);
if(JSON.stringify(out)!=='[\"newer\",\"older\"]')process.exit(1);
""")
@@ -162,7 +195,7 @@ if(JSON.stringify(out)!=='[\"newer\",\"older\"]')process.exit(1);
def test_signed_nanosecond_values_sort_exactly():
result = _run_sort_harness("""
S.workspaceSortKey='modified-desc';
const input=[{name:'negative-string',type:'file',mtime_ns:'-2'},{name:'zero',type:'file',mtime_ns:0},{name:'negative-number',type:'file',mtime_ns:-1}];
const input=[{name:'negative-string',type:'file',workspace_sort_rank:2,mtime_ns:'-2'},{name:'zero',type:'file',workspace_sort_rank:2,mtime_ns:0},{name:'negative-number',type:'file',workspace_sort_rank:2,mtime_ns:-1}];
const out=_workspaceEntriesForRender(input).map(x=>x.name);
if(JSON.stringify(out)!=='[\"zero\",\"negative-number\",\"negative-string\"]')process.exit(1);
""")
@@ -172,7 +205,7 @@ if(JSON.stringify(out)!=='[\"zero\",\"negative-number\",\"negative-string\"]')pr
def test_signed_zero_and_leading_zero_strings_compare_equal():
result = _run_sort_harness("""
S.workspaceSortKey='modified-desc';
const input=[{name:'zero',type:'file',mtime_ns:'0'},{name:'negative-zero',type:'file',mtime_ns:'-0'},{name:'positive-zero',type:'file',mtime_ns:'+0'},{name:'leading-zero',type:'file',mtime_ns:'000'},{name:'negative-leading-zero',type:'file',mtime_ns:'-00'}];
const input=[{name:'zero',type:'file',workspace_sort_rank:2,mtime_ns:'0'},{name:'negative-zero',type:'file',workspace_sort_rank:2,mtime_ns:'-0'},{name:'positive-zero',type:'file',workspace_sort_rank:2,mtime_ns:'+0'},{name:'leading-zero',type:'file',workspace_sort_rank:2,mtime_ns:'000'},{name:'negative-leading-zero',type:'file',workspace_sort_rank:2,mtime_ns:'-00'}];
if(['0','-0','+0','000','-00'].some(value=>_workspaceEntryTimestampKey({mtime_ns:value},'mtime_ns')!=='0'))process.exit(1);
const out=_workspaceEntriesForRender(input).map(x=>x.name);
if(JSON.stringify(out)!=='[\"zero\",\"negative-zero\",\"positive-zero\",\"leading-zero\",\"negative-leading-zero\"]')process.exit(1);
@@ -210,7 +243,7 @@ def test_created_pref_survives_unavailable():
const mk=()=>({hidden:true,attrs:{},setAttribute(k,v){this.attrs[k]=String(v);},removeAttribute(k){delete this.attrs[k];}});
const ind=mk(),dot=mk();
S.showHiddenWorkspaceFiles=false;S._workspaceBirthtimeSeen=false;S.workspaceSortKey='created-desc';
const input=[{name:'b',type:'file',birthtime_ns:null},{name:'a',type:'file',birthtime_ns:null}];
const input=[{name:'b',type:'file',workspace_sort_rank:2,birthtime_ns:null},{name:'a',type:'file',workspace_sort_rank:2,birthtime_ns:null}];
_noteWorkspaceBirthtimeSupport(input);
const out=_workspaceEntriesForRender(input).map(x=>x.name);
_syncWorkspacePrefsIndicators(ind,dot);
@@ -243,7 +276,7 @@ const row={attrs:{},classList:{toggle(){}},setAttribute(k,v){this.attrs[k]=Strin
_workspacePrefsMenu={querySelectorAll(sel){return sel==='.workspace-prefs-item--radio'?[row]:[];}};
S.session={workspace:'/one'};
_syncWorkspaceBirthtimeSupportScope(S.session.workspace);
_noteWorkspaceBirthtimeSupport([{name:'seen',type:'file',birthtime_ns:'1752598800000000001'}]);
_noteWorkspaceBirthtimeSupport([{name:'seen',type:'file',workspace_sort_rank:2,birthtime_ns:'1752598800000000001'}]);
if(_workspaceCreatedSortAvailable()!==true)process.exit(1);
S.session={workspace:'/two'};
_syncWorkspaceBirthtimeSupportScope(S.session.workspace);
@@ -9,6 +9,7 @@ def test_browser_workspace_entries_stringify_nanosecond_fields():
"name": "newer.txt",
"path": "newer.txt",
"type": "file",
"workspace_sort_rank": 2,
"mtime_ns": 1_752_598_800_000_000_001,
"birthtime_ns": 1_752_598_800_000_000_000,
},
@@ -16,6 +17,7 @@ def test_browser_workspace_entries_stringify_nanosecond_fields():
"name": "missing.txt",
"path": "missing.txt",
"type": "file",
"workspace_sort_rank": 1,
"mtime_ns": None,
"birthtime_ns": None,
},
@@ -27,5 +29,7 @@ def test_browser_workspace_entries_stringify_nanosecond_fields():
assert payload[0]["birthtime_ns"] == "1752598800000000000"
assert payload[1]["mtime_ns"] is None
assert payload[1]["birthtime_ns"] is None
assert payload[0]["workspace_sort_rank"] == 2
assert payload[1]["workspace_sort_rank"] == 1
assert entries[0]["mtime_ns"] == 1_752_598_800_000_000_001
assert entries[0]["birthtime_ns"] == 1_752_598_800_000_000_000
+8
View File
@@ -24,3 +24,11 @@ def test_directory_signature_can_be_computed_from_supplied_entries(tmp_path):
entries = list_dir(tmp_path, ".")
assert dir_signature(tmp_path, ".", entries) == dir_signature(tmp_path, ".", entries)
def test_directory_signature_ignores_workspace_sort_rank(tmp_path):
(tmp_path / "alpha.txt").write_text("one", encoding="utf-8")
entries = list_dir(tmp_path, ".")
changed = [dict(entry, workspace_sort_rank=1) for entry in entries]
assert dir_signature(tmp_path, ".", entries) == dir_signature(tmp_path, ".", changed)