"""Focused regression coverage for workspace sorting and birthtime metadata.""" import hashlib import json import os import subprocess from pathlib import Path import pytest from api import workspace as workspace_api ROOT = Path(__file__).resolve().parents[1] UI_JS = (ROOT / "static" / "ui.js").read_text(encoding="utf-8") I18N_JS = (ROOT / "static" / "i18n.js").read_text(encoding="utf-8") def test_list_dir_emits_birthtime_ns(tmp_path): (tmp_path / "file.txt").write_text("x", encoding="utf-8") (tmp_path / "folder").mkdir() entries = workspace_api.list_dir(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): birthtime_ns = getattr(workspace_api, "_birthtime_ns", None) assert birthtime_ns is not None, "_birthtime_ns helper is missing" class Stat: pass modern = Stat() modern.st_birthtime_ns = 123 assert birthtime_ns(modern) == 123 mac = Stat() mac.st_birthtime = 1.5 assert birthtime_ns(mac) == 1_500_000_000 windows = Stat() windows.st_ctime_ns = 456 monkeypatch.setattr("api.workspace.sys.platform", "win32") assert birthtime_ns(windows) == 456 linux = Stat() monkeypatch.setattr("api.workspace.sys.platform", "linux") assert birthtime_ns(linux) is None def test_escape_symlink_birthtime_is_link_local(tmp_path): outside = tmp_path / "outside.txt" outside.write_text("outside", encoding="utf-8") workspace = tmp_path / "workspace" workspace.mkdir() link = workspace / "escape.txt" try: link.symlink_to(outside) except (OSError, NotImplementedError) as exc: pytest.skip(f"symlink unavailable: {exc}") entry = next(item for item in workspace_api.list_dir(workspace, ".") if item["name"] == "escape.txt") assert entry["target_outside_workspace"] is True assert entry["workspace_sort_rank"] == 0 birthtime_ns = getattr(workspace_api, "_birthtime_ns", None) assert birthtime_ns is not None, "_birthtime_ns helper is missing" assert entry["birthtime_ns"] == birthtime_ns(link.lstat()) assert "target" not in entry and "size" not in entry def test_dir_signature_unchanged_by_birthtime(tmp_path): (tmp_path / "same.txt").write_text("same", encoding="utf-8") entries = workspace_api.list_dir(tmp_path, ".") expected = hashlib.sha256( json.dumps( [ { "name": entry.get("name"), "path": entry.get("path"), "type": entry.get("type"), "is_dir": entry.get("is_dir"), "size": entry.get("size"), "mtime_ns": entry.get("mtime_ns"), "target": entry.get("target"), "target_outside_workspace": entry.get("target_outside_workspace"), } for entry in entries ], sort_keys=True, separators=(",", ":"), ensure_ascii=False, ).encode("utf-8") ).hexdigest() assert workspace_api.dir_signature(tmp_path, ".", entries) == expected def _run_sort_harness(body): start = UI_JS.index("const WORKSPACE_SORT_KEYS=") end = UI_JS.index("// ── Workspace preferences kebab menu", start) source = UI_JS[start:end] script = """ global.S={session:{workspace:'/test'},showHiddenWorkspaceFiles:true,workspaceSortKey:'name-asc'}; global.localStorage={getItem(key){return key==='hermes-workspace-show-hidden-files'?'1':null},setItem(){}}; global.$=()=>null; function _visibleWorkspaceEntries(entries){return S.showHiddenWorkspaceFiles?entries:entries.filter(()=>true)} """ + source + "\n" + body return subprocess.run(["node", "-e", script], capture_output=True, text=True, check=False) def test_sort_modified_desc_within_rank(): result = _run_sort_harness(""" S.workspaceSortKey='modified-desc'; 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); """) assert result.returncode == 0, result.stderr def test_sort_name_desc_within_rank(): result = _run_sort_harness(""" S.workspaceSortKey='name-desc'; 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); """) assert result.returncode == 0, result.stderr def test_name_asc_is_identity(): result = _run_sort_harness(""" const input=[{name:'b',type:'file'},{name:'a',type:'file'}]; const out=_workspaceEntriesForRender(input); if(out!==input||out[0]!==input[0])process.exit(1); """) assert result.returncode == 0, result.stderr def test_rank_grouping_holds_all_keys(): result = _run_sort_harness(""" 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:'2garbage'})!==1||_workspaceEntryRank({type:'file',workspace_sort_rank:2.5})!==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 def test_no_workspace_render_clears_birthtime_support(): start = UI_JS.index("function renderFileTree(){") end = UI_JS.index("let _wsActiveDragPath=", start) render_source = UI_JS[start:end] sort_start = UI_JS.index("const WORKSPACE_SORT_KEYS=") sort_end = UI_JS.index("// ── Workspace preferences kebab menu", sort_start) sort_source = UI_JS[sort_start:sort_end] script = """ const box={scrollTop:0,innerHTML:'',style:{}}; const empty={textContent:'',style:{}}; global.S={session:null,entries:[],currentDir:'.',_dirCache:{},showHiddenWorkspaceFiles:true,workspaceSortKey:'created-desc',_workspaceBirthtimeSeen:true,_workspaceBirthtimeWorkspace:'/old'}; global.localStorage={getItem(){return null},setItem(){}}; global.$=id=>id==='fileTree'?box:id==='wsEmptyState'?empty:null; global.t=()=>''; var _workspacePrefsMenu=null; """ + sort_source + render_source + """ renderFileTree(); if(S._workspaceBirthtimeSeen!==false||S._workspaceBirthtimeWorkspace!=='')process.exit(1); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True, check=False) assert result.returncode == 0, result.stderr def test_session_teardown_disables_created_sort(): result = _run_sort_harness(""" S._workspaceBirthtimeSeen=true; if(_workspaceCreatedSortAvailable()!==true)process.exit(1); S.session=null; if(_workspaceCreatedSortAvailable()!==false)process.exit(1); """) assert result.returncode == 0, result.stderr def test_missing_timestamps_sort_last(): result = _run_sort_harness(""" S.workspaceSortKey='created-desc';S._workspaceBirthtimeSeen=true; 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); """) assert result.returncode == 0, result.stderr def test_large_nanosecond_strings_sort_exactly(): result = _run_sort_harness(""" S.workspaceSortKey='modified-desc'; 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); """) assert result.returncode == 0, result.stderr def test_signed_nanosecond_values_sort_exactly(): result = _run_sort_harness(""" S.workspaceSortKey='modified-desc'; 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); """) assert result.returncode == 0, result.stderr def test_signed_zero_and_leading_zero_strings_compare_equal(): result = _run_sort_harness(""" S.workspaceSortKey='modified-desc'; 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); """) assert result.returncode == 0, result.stderr @pytest.mark.parametrize("value", ["", " ", "null", "{\"k\":1}", "created"]) def test_stored_sort_key_normalization(value): result = _run_sort_harness(f""" if(!_normalizeWorkspaceSortKey({json.dumps(value)})||!WORKSPACE_SORT_KEYS.includes(_normalizeWorkspaceSortKey({json.dumps(value)})))process.exit(1); """) assert result.returncode == 0, result.stderr def test_prefs_indicators_or_logic(): result = _run_sort_harness(""" 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.workspaceSortKey='name-asc'; _syncWorkspacePrefsIndicators(ind,dot); if(ind.hidden!==true||dot.hidden!==true)process.exit(1); S.workspaceSortKey='modified-desc'; _syncWorkspacePrefsIndicators(ind,dot); if(ind.hidden!==true||dot.hidden!==false)process.exit(1); S.showHiddenWorkspaceFiles=true;S.workspaceSortKey='name-asc'; _syncWorkspacePrefsIndicators(ind,dot); if(ind.hidden!==false||dot.hidden!==false)process.exit(1); """) assert result.returncode == 0, result.stderr def test_created_pref_survives_unavailable(): result = _run_sort_harness(""" 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',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); if(S._workspaceBirthtimeSeen!==false||_workspaceCreatedSortAvailable()!==false||_effectiveWorkspaceSortKey()!=='name-asc'||S.workspaceSortKey!=='created-desc'||JSON.stringify(out)!=='[\"b\",\"a\"]'||ind.hidden!==true||dot.hidden!==true)process.exit(1); """) assert result.returncode == 0, result.stderr def test_unavailable_created_pref_uses_effective_menu_radio_state(): result = _run_sort_harness(""" const mkRow=value=>{ const input={value,checked:false,disabled:false}; return {_input:input,attrs:{},classList:{toggle(){}},setAttribute(k,v){this.attrs[k]=String(v);},querySelector(sel){return sel==='input[name="workspaceSortKey"]'?input:null;}}; }; const nameRow=mkRow('name-asc'); const createdRow=mkRow('created-desc'); _workspacePrefsMenu={querySelectorAll(sel){return sel==='.workspace-prefs-item--radio'?[nameRow,createdRow]:[];}}; S._workspaceBirthtimeSeen=false; S.workspaceSortKey='created-desc'; _syncWorkspaceSortMenuState(); if(nameRow._input.checked!==true||nameRow.attrs['aria-checked']!=='true'||createdRow._input.checked!==false||createdRow._input.disabled!==true||createdRow.attrs['aria-checked']!=='false'||createdRow.attrs['aria-disabled']!=='true')process.exit(1); """) assert result.returncode == 0, result.stderr def test_birthtime_support_scope_resets_on_workspace_change(): result = _run_sort_harness(""" const input={value:'created-desc',checked:true,disabled:false}; const row={attrs:{},classList:{toggle(){}},setAttribute(k,v){this.attrs[k]=String(v);},querySelector(sel){return sel==='input[name="workspaceSortKey"]'?input:null;}}; _workspacePrefsMenu={querySelectorAll(sel){return sel==='.workspace-prefs-item--radio'?[row]:[];}}; S.session={workspace:'/one'}; _syncWorkspaceBirthtimeSupportScope(S.session.workspace); _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); if(S._workspaceBirthtimeSeen!==false||_workspaceCreatedSortAvailable()!==false||input.disabled!==true||row.attrs['aria-disabled']!=='true')process.exit(1); """) assert result.returncode == 0, result.stderr def test_birthtime_support_scope_reconciles_open_menu_metadata(): result = _run_sort_harness(""" global.t=key=>key==='workspace_sort_created_unavailable'?'Creation unavailable':key; global.document={createElement(){return {className:'',textContent:'',remove(){this.removed=true;},};}}; const meta={textContent:'Creation unavailable',remove(){this.removed=true;}}; const copy={children:[meta],appendChild(node){this.children.push(node);}}; const input={value:'created-desc',checked:false,disabled:true}; const row={attrs:{},classList:{toggle(){}},setAttribute(k,v){this.attrs[k]=String(v);},querySelector(sel){ if(sel==='input[name="workspaceSortKey"]')return input; if(sel==='.workspace-prefs-copy')return copy; if(sel==='.workspace-prefs-meta')return copy.children.find(node=>!node.removed)||null; return null; }}; _workspacePrefsMenu={querySelectorAll(sel){return sel==='.workspace-prefs-item--radio'?[row]:[];}}; S._workspaceBirthtimeSeen=false; _syncWorkspaceSortMenuState(); if(!row.querySelector('.workspace-prefs-meta')||row.querySelector('.workspace-prefs-meta').textContent!=='Creation unavailable')process.exit(1); S._workspaceBirthtimeSeen=true; _syncWorkspaceSortMenuState(); if(row.querySelector('.workspace-prefs-meta'))process.exit(1); S._workspaceBirthtimeSeen=false; _syncWorkspaceSortMenuState(); const recreated=row.querySelector('.workspace-prefs-meta'); if(!recreated||recreated.textContent!=='Creation unavailable')process.exit(1); """) assert result.returncode == 0, result.stderr def test_set_workspace_sort_key_updates_open_menuitemradio_state(): result = _run_sort_harness(""" global.renderFileTree=()=>{}; const rows=['name-asc','modified-desc'].map(value=>{ const input={value,checked:false}; return {_input:input,attrs:{},setAttribute(k,v){this.attrs[k]=String(v);},querySelector(sel){return sel==='input[name="workspaceSortKey"]'?input:null;}}; }); _workspacePrefsMenu={querySelectorAll(sel){return sel==='.workspace-prefs-item--radio'?rows:[];}}; S.workspaceSortKey='name-asc'; setWorkspaceSortKey('modified-desc'); if(rows[0].attrs['aria-checked']!=='false'||rows[1].attrs['aria-checked']!=='true'||rows[0]._input.checked!==false||rows[1]._input.checked!==true)process.exit(1); """) assert result.returncode == 0, result.stderr def test_sort_public_surface_text_shape(): for key in ("WORKSPACE_SORT_KEYS", "setWorkspaceSortKey", "hermes-workspace-sort-key", "workspace_sort_by", "workspace_sort_name_asc", "workspace_sort_name_desc", "workspace_sort_created_desc", "workspace_sort_modified_desc", "workspace_sort_created_unavailable"): assert key in UI_JS or key in I18N_JS assert UI_JS.count("const active=_effectiveWorkspaceSortKey();") >= 2 assert "row.setAttribute('aria-disabled',disabled?'true':'false');" in UI_JS assert "Number(a[field])" not in UI_JS assert I18N_JS.count("workspace_sort_by:") == I18N_JS.count("workspace_show_hidden_files:")