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:
1 parent
573ac33182
commit
88c4b9a6b8
5 files changed
+64
-14
No files matched your search
@@ -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
@@ -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];
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
Reference in new issue
Block a user