import json import subprocess from html.parser import HTMLParser from pathlib import Path from urllib.parse import urljoin from tests.dashboard_bundle import dashboard_bundle_text FRONTEND = Path(__file__).parents[1] / "frontend" COMMANDS = FRONTEND / "commands.js" SEARCH_PREVIEW = FRONTEND / "search-preview.js" MOBILE_SEARCH_VIEWPORT = FRONTEND / "mobile-search-viewport.js" SEARCH_DEFER = FRONTEND / "search-defer.js" class ScriptSourceParser(HTMLParser): def __init__(self): super().__init__() self.sources = [] def handle_starttag(self, tag, attrs): if tag == "script": source = dict(attrs).get("src") if source: self.sources.append(source) def test_filtered_command_runs_the_matching_action(): script = f""" const filterCommands = require({json.dumps(str(COMMANDS))}); let action = ''; const commands = [ {{ name: 'Open whiteboard', run: () => {{ action = 'whiteboard'; }} }}, {{ name: 'Refresh now', run: () => {{ action = 'refresh'; }} }}, ]; const matches = filterCommands(commands, 'refresh'); if (matches.length !== 1) throw new Error(`expected one match, got ${{matches.length}}`); matches[0].run(); if (action !== 'refresh') throw new Error(`expected refresh, got ${{action}}`); """ subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True) def test_command_script_resolves_inside_dashboard_subpath(): parser = ScriptSourceParser() parser.feed(dashboard_bundle_text()) command_source = next(source for source in parser.sources if source.endswith("commands.js")) assert urljoin( "https://forge.alexanderwhitestone.com/dashboard/", command_source ) == "https://forge.alexanderwhitestone.com/dashboard/static/commands.js" def test_mobile_search_renders_touch_sized_type_and_status_scope_controls(): html = dashboard_bundle_text() css = (FRONTEND / "dashboard.css").read_text() commands = COMMANDS.read_text() assert '
Filter search results' in html assert '' in html assert 'Repository' in html assert '' in html assert "api/v1/repositories/search?q=" in html assert "repository:qs('#cmd-search-repository').value" in html assert "'&repository=' + encodeURIComponent(scope.repository)" in commands assert '#cmd-search-repository' in css def test_mobile_search_preview_exposes_a_keyboard_safe_reply_and_continue_composer(): html = dashboard_bundle_text() css = (FRONTEND / "dashboard.css").read_text() preview = SEARCH_PREVIEW.read_text() assert '
{{ const pending = []; const controller = filterCommands.createGlobalSearchController({{ delay: 0, search: (query, signal, page, scope) => new Promise(resolve => pending.push({{signal, scope, resolve}})), onState() {{}}, }}); controller.setQuery('mobile'); await new Promise(resolve => setTimeout(resolve, 0)); controller.setScope({{kind:'all', state:'all', repository:'stackchain/api'}}); await new Promise(resolve => setTimeout(resolve, 0)); if (!pending[0].signal.aborted) throw new Error('repository change did not abort old request'); if (pending[1].scope.repository !== 'stackchain/api') throw new Error('repository was not forwarded'); pending[1].resolve({{items:[],has_more:false,next_page:2}}); }})().catch(error => {{ console.error(error); process.exit(1); }}); """ subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True) def test_remote_command_search_ignores_stale_responses(): script = f""" const filterCommands = require({json.dumps(str(COMMANDS))}); (async () => {{ const pending = new Map(); const states = []; const controller = filterCommands.createGlobalSearchController({{ delay: 0, search: query => new Promise(resolve => pending.set(query, resolve)), onState: state => states.push(state), }}); controller.setQuery('mobile'); await new Promise(resolve => setTimeout(resolve, 0)); controller.setQuery('release'); await new Promise(resolve => setTimeout(resolve, 0)); pending.get('release')([{{ title: 'Current result' }}]); await new Promise(resolve => setTimeout(resolve, 0)); pending.get('mobile')([{{ title: 'Stale result' }}]); await new Promise(resolve => setTimeout(resolve, 0)); const ready = states.filter(state => state.status === 'ready'); if (ready.length !== 1) throw new Error(`expected one ready state, got ${{ready.length}}`); if (ready[0].query !== 'release' || ready[0].items[0].title !== 'Current result') {{ throw new Error(`stale response replaced current state: ${{JSON.stringify(ready)}}`); }} }})().catch(error => {{ console.error(error); process.exit(1); }}); """ subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True, ) def test_remote_command_search_scope_change_aborts_and_restarts_without_stale_results(): script = f""" const filterCommands = require({json.dumps(str(COMMANDS))}); (async () => {{ const pending = []; const states = []; const controller = filterCommands.createGlobalSearchController({{ delay: 0, search: (query, signal, page, scope) => new Promise(resolve => pending.push({{query, signal, page, scope, resolve}})), onState: state => states.push(state), }}); controller.setQuery('mobile'); await new Promise(resolve => setTimeout(resolve, 0)); controller.setScope({{kind:'pull', state:'open'}}); await new Promise(resolve => setTimeout(resolve, 0)); if (!pending[0].signal.aborted) throw new Error('scope change did not abort old request'); if (JSON.stringify(pending[1].scope) !== JSON.stringify({{kind:'pull', state:'open'}})) {{ throw new Error('new request did not receive scope'); }} pending[1].resolve({{items:[{{kind:'pull',repository:'a/b',number:2}}],has_more:false,next_page:2}}); pending[0].resolve({{items:[{{kind:'issue',repository:'a/b',number:1}}],has_more:false,next_page:2}}); await new Promise(resolve => setTimeout(resolve, 0)); const ready = states.filter(state => state.status === 'ready').at(-1); if (ready.items.length !== 1 || ready.items[0].kind !== 'pull') throw new Error('stale scope result entered list'); if (ready.scope.kind !== 'pull' || ready.scope.state !== 'open') throw new Error('scope missing from state'); }})().catch(error => {{ console.error(error); process.exit(1); }}); """ subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True) def test_remote_command_search_aborts_superseded_and_cleared_queries(): script = f""" const filterCommands = require({json.dumps(str(COMMANDS))}); (async () => {{ const signals = []; const states = []; const controller = filterCommands.createGlobalSearchController({{ delay: 0, search: (query, signal) => {{ signals.push(signal); return new Promise((resolve, reject) => signal.addEventListener('abort', () => {{ const error = new Error('aborted'); error.name = 'AbortError'; reject(error); }})); }}, onState: state => states.push(state), }}); controller.setQuery('mobile'); await new Promise(resolve => setTimeout(resolve, 0)); controller.setQuery('release'); if (!signals[0].aborted) throw new Error('superseded request was not aborted'); await new Promise(resolve => setTimeout(resolve, 0)); controller.setQuery(''); if (!signals[1].aborted) throw new Error('cleared query request was not aborted'); await new Promise(resolve => setTimeout(resolve, 0)); if (states.some(state => state.status === 'error')) throw new Error('abort rendered an error'); if (states.at(-1).status !== 'idle') throw new Error('clear did not restore idle state'); }})().catch(error => {{ console.error(error); process.exit(1); }}); """ subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True) def test_remote_command_search_preserves_partial_result_status(): script = f""" const filterCommands = require({json.dumps(str(COMMANDS))}); (async () => {{ const states = []; const controller = filterCommands.createGlobalSearchController({{ delay: 0, search: () => Promise.resolve({{ items:[{{ title:'Useful result' }}], partial:true }}), onState: state => states.push(state), }}); controller.setQuery('mobile'); await new Promise(resolve => setTimeout(resolve, 0)); await new Promise(resolve => setTimeout(resolve, 0)); const ready = states.find(state => state.status === 'ready'); if (!ready || ready.items.length !== 1 || ready.partial !== true) {{ throw new Error('partial result metadata was lost: ' + JSON.stringify(states)); }} }})().catch(error => {{ console.error(error); process.exit(1); }}); """ subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True) def test_remote_command_search_retries_partial_continuation_without_losing_results(): script = f""" const filterCommands = require({json.dumps(str(COMMANDS))}); (async () => {{ const requests = []; const states = []; const controller = filterCommands.createGlobalSearchController({{ delay: 0, search: (query, signal, page, scope, continuation) => {{ requests.push({{page, continuation}}); if (requests.length === 1) return Promise.resolve({{ items:[{{kind:'pull',repository:'a/b',number:2}}], partial:true, has_more:true, next_page:2, continuation:{{issues:1,pulls:null}}, failed_streams:['issue'] }}); return Promise.resolve({{ items:[{{kind:'issue',repository:'a/b',number:1}}], partial:false, has_more:false, next_page:2, continuation:{{issues:null,pulls:null}}, failed_streams:[] }}); }}, onState: state => states.push(state), }}); controller.setQuery('mobile'); await new Promise(resolve => setTimeout(resolve, 0)); await new Promise(resolve => setTimeout(resolve, 0)); await controller.loadMore(); const ready = states.filter(state => state.status === 'ready').at(-1); if (JSON.stringify(requests[1].continuation) !== JSON.stringify({{issues:1,pulls:null}})) {{ throw new Error('retry did not preserve failed stream page: ' + JSON.stringify(requests)); }} if (ready.partial || ready.items.length !== 2 || ready.items[0].kind !== 'pull' || ready.items[1].kind !== 'issue') {{ throw new Error('recovered results were not appended: ' + JSON.stringify(ready)); }} }})().catch(error => {{ console.error(error); process.exit(1); }}); """ subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True) def test_remote_command_search_appends_deduplicated_pages_and_ignores_stale_load_more(): script = f""" const filterCommands = require({json.dumps(str(COMMANDS))}); (async () => {{ const pending = new Map(); const states = []; const controller = filterCommands.createGlobalSearchController({{ delay: 0, search: (query, signal, page) => new Promise(resolve => pending.set(query + ':' + page, resolve)), onState: state => states.push(state), }}); controller.setQuery('mobile'); await new Promise(resolve => setTimeout(resolve, 0)); pending.get('mobile:1')({{ items:[{{kind:'issue',repository:'a/b',number:1}}], has_more:true, next_page:2 }}); await new Promise(resolve => setTimeout(resolve, 0)); controller.loadMore(); await new Promise(resolve => setTimeout(resolve, 0)); controller.setQuery('release'); await new Promise(resolve => setTimeout(resolve, 0)); pending.get('mobile:2')({{ items:[{{kind:'issue',repository:'a/b',number:1}},{{kind:'pull',repository:'a/b',number:2}}], has_more:false, next_page:3 }}); pending.get('release:1')({{ items:[{{kind:'issue',repository:'a/b',number:3}}], has_more:false, next_page:2 }}); await new Promise(resolve => setTimeout(resolve, 0)); const ready = states.filter(state => state.status === 'ready').at(-1); if (ready.query !== 'release' || ready.items.length !== 1 || ready.items[0].number !== 3) {{ throw new Error('stale page entered current query: ' + JSON.stringify(states)); }} }})().catch(error => {{ console.error(error); process.exit(1); }}); """ subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True) def test_command_selection_wraps_for_arrow_keys(): script = f""" const commands = require({json.dumps(str(COMMANDS))}); if (commands.searchUrl('mobile', 2, {{kind:'all',state:'open'}}, {{issues:1,pulls:3}}) !== 'api/v1/search?q=mobile&limit=10&page=2&kind=all&state=open&issues_page=1&pulls_page=3') {{ throw new Error('independent continuation query was not encoded'); }} if (commands.nextSelection(-1, 'ArrowDown', 3) !== 0) throw new Error('should select first'); if (commands.nextSelection(2, 'ArrowDown', 3) !== 0) throw new Error('should wrap down'); if (commands.nextSelection(0, 'ArrowUp', 3) !== 2) throw new Error('should wrap up'); if (commands.nextSelection(1, 'Enter', 3) !== 1) throw new Error('other keys should not move'); """ subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True) def test_palette_exposes_accessible_global_work_search_under_dashboard_subpath(): html = dashboard_bundle_text() assert 'role="combobox"' in html assert 'aria-controls="cmd-results"' in html assert 'role="listbox"' in html assert "fetch(filterCommands.searchUrl(" in html assert "async function searchGlobalWork(query, signal, page = 1, scope" in html assert "signal," in html assert "Some results are temporarily unavailable." in html assert "Retry missing results" in html assert 'id="cmd-load-more"' in html assert urljoin( "https://forge.alexanderwhitestone.com/dashboard/", "api/v1/search?q=mobile", ) == "https://forge.alexanderwhitestone.com/dashboard/api/v1/search?q=mobile" def test_search_preview_navigates_a_loaded_result_session_and_reports_position(): script = f""" const createSearchPreview = require({json.dumps(str(SEARCH_PREVIEW))}); (async () => {{ const items = [1, 2, 3].map(number => ({{repository:'stackchain/api', number, kind:'issue'}})); const states = []; const navigated = []; const preview = createSearchPreview({{ fetchJson: item => Promise.resolve({{...item, title:'Issue ' + item.number}}), mutate: () => Promise.resolve(), getSession: () => ({{items, more:false}}), loadMore: () => Promise.resolve(), onNavigate: item => navigated.push(item.number), onState: state => states.push(state), }}); await preview.open(items[1]); await preview.previous(); await preview.next(); const ready = states.filter(state => state.status === 'ready'); process.stdout.write(JSON.stringify({{ navigated, positions:ready.map(state => state.navigation && [state.navigation.position, state.navigation.total, state.navigation.hasPrevious, state.navigation.hasNext]), current:ready.at(-1).detail.number, }})); }})().catch(error => {{ console.error(error); process.exit(1); }}); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "navigated": [1, 2], "positions": [[2, 3, True, True], [1, 3, False, True], [2, 3, True, True]], "current": 2, } def test_search_preview_queue_and_next_advances_only_after_durable_admission(): script = f""" const createSearchPreview = require({json.dumps(str(SEARCH_PREVIEW))}); (async()=>{{ const items=[1,2].map(number=>({{repository:'stackchain/api',number,kind:'issue'}})); const values=new Map();let admit;const navigated=[];const states=[];let cleared=0; const preview=createSearchPreview({{ fetchJson:async item=>item,mutate:async()=>{{}}, queueReply:(item,body,operationId)=>new Promise(resolve=>{{admit=()=>resolve({{id:operationId,queued:true}});}}), storage:{{getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)}}, createOperationId:()=> 'offline-search-1',hasAttachments:()=>false,clearAttachments:()=>{{cleared+=1;}}, getSession:()=>({{items,more:false}}),onNavigate:item=>navigated.push(item.number),onState:state=>states.push(state), }}); await preview.open(items[0]);preview.saveReplyDraft('Queue safely.'); const pending=preview.reply({{advance:true}});await Promise.resolve();const before=[...navigated];admit();await pending; process.stdout.write(JSON.stringify({{before,navigated,cleared,draft:values.size, queued:states.some(state=>state.status==='queued')}})); }})().catch(error=>{{console.error(error);process.exit(1);}}); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "before": [], "navigated": [2], "cleared": 1, "draft": 0, "queued": True } def test_search_preview_reply_retry_keeps_draft_and_operation_until_server_confirmation(): script = f""" const createSearchPreview = require({json.dumps(str(SEARCH_PREVIEW))}); (async () => {{ const item = {{repository:'stackchain/api', number:42, kind:'pull'}}; const values = new Map(); const storage = {{ getItem:key => values.has(key) ? values.get(key) : null, setItem:(key,value) => values.set(key,value), removeItem:key => values.delete(key), }}; const calls = []; const states = []; let attempt = 0; const preview = createSearchPreview({{ fetchJson: candidate => Promise.resolve({{...candidate, title:'Review reply'}}), mutate: () => Promise.resolve(), postReply: (candidate, body, operationId) => {{ calls.push([candidate.number, body, operationId]); attempt += 1; return attempt === 1 ? Promise.reject(new Error('offline')) : Promise.resolve({{ id:91, author:'timmy', body, created_at:'2026-08-14T12:30:00Z' }}); }}, storage, createOperationId: () => 'reply-operation-42', onState: state => states.push(state), }}); await preview.open(item); preview.saveReplyDraft('Ready to merge.'); try {{ await preview.reply(); }} catch (_) {{}} const draftAfterFailure = preview.replyDraft(); await preview.reply(); process.stdout.write(JSON.stringify({{ calls, draftAfterFailure, draftAfterSuccess:preview.replyDraft(), comments:states.filter(state => state.conversation).at(-1).conversation.comments, }})); }})().catch(error => {{ console.error(error); process.exit(1); }}); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "calls": [ [42, "Ready to merge.", "reply-operation-42"], [42, "Ready to merge.", "reply-operation-42"], ], "draftAfterFailure": "Ready to merge.", "draftAfterSuccess": "", "comments": [{ "id": 91, "author": "timmy", "body": "Ready to merge.", "created_at": "2026-08-14T12:30:00Z", }], } def test_search_preview_reply_and_next_advances_only_after_server_confirmation(): script = f""" const createSearchPreview = require({json.dumps(str(SEARCH_PREVIEW))}); (async () => {{ const items = [1, 2].map(number => ({{repository:'stackchain/api', number, kind:'issue'}})); const values = new Map(); let confirm; const navigated = []; const preview = createSearchPreview({{ fetchJson:item => Promise.resolve(item), mutate:() => Promise.resolve(), postReply:() => new Promise(resolve => {{ confirm = resolve; }}), storage:{{getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)}}, createOperationId:() => 'reply-next-1', getSession:() => ({{items,more:false}}), loadMore:() => Promise.resolve(), onNavigate:item => navigated.push(item.number), onState:() => {{}}, }}); await preview.open(items[0]); preview.saveReplyDraft('Done.'); const pending = preview.reply({{advance:true}}); await Promise.resolve(); const before = [...navigated]; confirm({{id:7,author:'timmy',body:'Done.'}}); await pending; process.stdout.write(JSON.stringify({{before,after:navigated,draft:preview.replyDraft()}})); }})().catch(error => {{ console.error(error); process.exit(1); }}); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "before": [], "after": [2], "draft": "", } def test_search_preview_keeps_confirmed_reply_when_earlier_conversation_load_finishes_late(): script = f""" const createSearchPreview = require({json.dumps(str(SEARCH_PREVIEW))}); (async () => {{ const values = new Map(); let finishConversation; const states = []; const preview = createSearchPreview({{ fetchJson:item => Promise.resolve({{...item,title:'Race'}}), fetchConversation:() => new Promise(resolve => {{ finishConversation = resolve; }}), mutate:() => Promise.resolve(), postReply:(_item,body) => Promise.resolve({{id:9,author:'timmy',body}}), storage:{{getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)}}, createOperationId:() => 'reply-race-9', onState:state => states.push(state), }}); await preview.open({{repository:'stackchain/api',number:9,kind:'issue'}}); preview.saveReplyDraft('Confirmed reply'); await preview.reply(); finishConversation({{comments:[{{id:3,author:'alex',body:'Earlier'}}],older_page:null}}); await Promise.resolve(); const comments = states.filter(state => state.conversation?.status === 'ready').at(-1).conversation.comments; process.stdout.write(JSON.stringify(comments.map(comment => comment.id))); }})().catch(error => {{ console.error(error); process.exit(1); }}); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == [3, 9] def test_search_preview_next_loads_one_page_at_boundary_then_announces_completion(): script = f""" const createSearchPreview = require({json.dumps(str(SEARCH_PREVIEW))}); (async () => {{ let session = {{items:[1, 2].map(number => ({{repository:'stackchain/api', number, kind:'issue'}})), more:true}}; let loads = 0; const states = []; const preview = createSearchPreview({{ fetchJson: item => Promise.resolve(item), mutate: () => Promise.resolve(), getSession: () => session, loadMore: async () => {{ loads += 1; session = {{items:[...session.items, {{repository:'stackchain/api', number:3, kind:'issue'}}], more:false}}; }}, onNavigate() {{}}, onState: state => states.push(state), }}); await preview.open(session.items[1]); await preview.next(); await preview.next(); const final = states.at(-1); process.stdout.write(JSON.stringify({{loads, current:final.detail.number, complete:final.complete, navigation:final.navigation}})); }})().catch(error => {{ console.error(error); process.exit(1); }}); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "loads": 1, "current": 3, "complete": True, "navigation": {"position": 3, "total": 3, "hasPrevious": True, "hasNext": False}, } def test_remote_search_load_more_is_awaitable_for_preview_boundary_navigation(): script = f""" const commands = require({json.dumps(str(COMMANDS))}); (async () => {{ const states = []; const controller = commands.createGlobalSearchController({{ delay:0, search: async (_query, _signal, page) => page === 1 ? {{items:[{{kind:'issue',repository:'a/b',number:1}}], has_more:true, next_page:2}} : {{items:[{{kind:'issue',repository:'a/b',number:2}}], has_more:false, next_page:3}}, onState: state => states.push(state), }}); controller.setQuery('mobile'); await new Promise(resolve => setTimeout(resolve, 5)); const loaded = await controller.loadMore(); process.stdout.write(JSON.stringify({{numbers:loaded.items.map(item => item.number), more:loaded.more}})); }})().catch(error => {{ console.error(error); process.exit(1); }}); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == {"numbers": [1, 2], "more": False} def test_mobile_search_preview_progressively_renders_a_retryable_paged_conversation(): html = dashboard_bundle_text() css = (FRONTEND / "dashboard.css").read_text() preview_source = SEARCH_PREVIEW.read_text() assert 'id="search-preview-conversation"' in html assert 'id="search-preview-comments"' in html assert 'id="search-preview-conversation-status"' in html assert 'id="retry-search-preview-conversation"' in html assert 'id="load-older-search-preview-comments"' in html assert 'aria-live="polite"' in html assert "fetchConversation:" in html assert "api.retryConversation()" in preview_source assert "api.loadOlderConversation()" in preview_source assert "renderMarkdown(comment.body || '')" in preview_source assert ".search-preview-conversation" in css assert ".search-preview-comment" in css assert "min-height:44px" in css def test_mobile_search_preview_renders_session_navigation_and_advances_after_planning(): html = dashboard_bundle_text() css = (FRONTEND / "dashboard.css").read_text() assert 'id="previous-search-result"' in html assert 'id="search-preview-position"' in html assert 'id="next-search-result"' in html assert 'aria-label="Search result navigation"' in html assert ".search-preview-navigation" in css assert ".search-preview-header button, .search-preview-actions button" in css assert "session:[()=>commandSearchState, commandSearch, item=>taskOverlayHistory.update({preview:item})]" in html preview_source = SEARCH_PREVIEW.read_text() assert "api.previous()" in preview_source assert "api.next()" in preview_source defer_flow = html.split("if (context === 'search')", 1)[1].split( "if (context === 'detail')", 1 )[0] queue_flow = html.split( "qs('#queue-search-result').addEventListener('click'", 1 )[1].split("qs('#start-search-result')", 1)[0] assert "next()" in defer_flow assert "next()" in queue_flow assert "closeSearchPreview()" not in defer_flow assert "if (outcome === 'queued') await next()" in queue_flow def test_search_preview_ignores_stale_result_details(): script = f""" const createSearchPreview = require({json.dumps(str(SEARCH_PREVIEW))}); (async () => {{ const pending = new Map(); const states = []; const preview = createSearchPreview({{ fetchJson: item => new Promise(resolve => pending.set(item.number, resolve)), mutate: () => Promise.resolve(), onState: state => states.push(state), }}); preview.open({{ repository:'stackchain/api', number:1, kind:'issue' }}); preview.open({{ repository:'stackchain/api', number:2, kind:'issue' }}); pending.get(2)({{ number:2, title:'Current' }}); await new Promise(resolve => setTimeout(resolve, 0)); pending.get(1)({{ number:1, title:'Stale' }}); await new Promise(resolve => setTimeout(resolve, 0)); const ready = states.filter(state => state.status === 'ready'); if (ready.length !== 1 || ready[0].detail.number !== 2) {{ throw new Error('stale detail replaced current preview: ' + JSON.stringify(ready)); }} }})().catch(error => {{ console.error(error); process.exit(1); }}); """ subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True) def test_search_preview_publishes_detail_before_progressive_conversation_and_ignores_stale_comments(): script = f""" const createSearchPreview = require({json.dumps(str(SEARCH_PREVIEW))}); (async () => {{ const pending = new Map(); const states = []; const preview = createSearchPreview({{ fetchJson: item => Promise.resolve({{...item, title:'Issue ' + item.number}}), fetchConversation: item => new Promise(resolve => pending.set(item.number, resolve)), mutate: () => Promise.resolve(), onState: state => states.push(state), }}); await preview.open({{repository:'stackchain/api', number:1, kind:'issue'}}); const firstReady = states.find(state => state.status === 'ready' && state.detail?.number === 1); if (!firstReady || firstReady.conversation?.status !== 'loading') throw new Error('detail waited for conversation'); await preview.open({{repository:'stackchain/api', number:2, kind:'issue'}}); pending.get(1)({{comments:[{{id:1,body:'stale'}}], page:1, older_page:null}}); pending.get(2)({{comments:[{{id:2,body:'current'}}], page:1, older_page:null}}); await new Promise(resolve => setTimeout(resolve, 0)); const final = states.at(-1); process.stdout.write(JSON.stringify({{ number:final.detail.number, status:final.conversation.status, comments:final.conversation.comments.map(comment => comment.body), }})); }})().catch(error => {{ console.error(error); process.exit(1); }}); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "number": 2, "status": "ready", "comments": ["current"], } def test_search_preview_retries_failed_conversation_and_prepends_older_messages_without_duplicates(): script = f""" const createSearchPreview = require({json.dumps(str(SEARCH_PREVIEW))}); (async () => {{ let calls = 0; const states = []; const preview = createSearchPreview({{ fetchJson: item => Promise.resolve({{...item, title:'Issue'}}), fetchConversation: (_item, page) => {{ calls += 1; if (calls === 1) return Promise.reject(new Error('offline')); if (page === 1) return Promise.resolve({{comments:[{{id:1,body:'old'}},{{id:3,body:'duplicate'}}], older_page:null}}); return Promise.resolve({{comments:[{{id:3,body:'newer'}},{{id:4,body:'newest'}}], older_page:1}}); }}, mutate: () => Promise.resolve(), onState: state => states.push(state), }}); await preview.open({{repository:'stackchain/api', number:42, kind:'issue'}}); await new Promise(resolve => setTimeout(resolve, 0)); if (states.at(-1).conversation.status !== 'error') throw new Error('failure unavailable'); await preview.retryConversation(); await preview.loadOlderConversation(); const final = states.at(-1).conversation; process.stdout.write(JSON.stringify({{ calls, status:final.status, ids:final.comments.map(comment => comment.id), olderPage:final.olderPage, }})); }})().catch(error => {{ console.error(error); process.exit(1); }}); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "calls": 3, "status": "ready", "ids": [1, 3, 4], "olderPage": None, } def test_search_preview_claim_is_single_flight_and_reports_confirmation(): script = f""" const createSearchPreview = require({json.dumps(str(SEARCH_PREVIEW))}); (async () => {{ let claims = 0; let resolveClaim; const states = []; const preview = createSearchPreview({{ fetchJson: item => Promise.resolve(item), mutate: (_detail, action) => {{ if (action !== 'claim') throw new Error('wrong action'); claims += 1; return new Promise(resolve => {{ resolveClaim = resolve; }}); }}, onState: state => states.push(state), }}); await preview.open({{ repository:'stackchain/api', number:42, kind:'issue' }}); const detail = {{ repository:'stackchain/api', number:42, claimable:true }}; const first = preview.claim(detail); const second = preview.claim(detail); if (claims !== 1 || first !== second) throw new Error('claim was not single-flight'); resolveClaim({{ assignees:['timmy'] }}); await first; if (!states.some(state => state.status === 'claimed')) throw new Error('claim confirmation missing'); }})().catch(error => {{ console.error(error); process.exit(1); }}); """ subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True) def test_search_preview_reopen_is_single_flight_and_reports_confirmation(): script = f""" const createSearchPreview = require({json.dumps(str(SEARCH_PREVIEW))}); (async () => {{ let reopens = 0; let resolveReopen; const states = []; const preview = createSearchPreview({{ fetchJson: item => Promise.resolve(item), mutate: (_detail, action) => {{ if (action !== 'reopen') throw new Error('wrong action'); reopens += 1; return new Promise(resolve => {{ resolveReopen = resolve; }}); }}, onState: state => states.push(state), }}); const detail = {{ repository:'stackchain/api', number:42, reopenable:true }}; await preview.open(detail); const first = preview.reopen(detail); const second = preview.reopen(detail); if (reopens !== 1 || first !== second) throw new Error('reopen was not single-flight'); resolveReopen({{ state:'open', assignees:['timmy'] }}); await first; if (!states.some(state => state.status === 'reopened')) throw new Error('reopen confirmation missing'); }})().catch(error => {{ console.error(error); process.exit(1); }}); """ subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True) def test_search_preview_watch_is_single_flight_and_keeps_visible_context_on_failure(): script = f""" const createSearchPreview = require({json.dumps(str(SEARCH_PREVIEW))}); (async () => {{ let calls = 0; let rejectWatch; const states = []; const detail = {{repository:'stackchain/api',number:42,kind:'issue',claimable:true,watching:false}}; const preview = createSearchPreview({{ fetchJson:async()=>detail, mutate:async()=>{{}}, watch:()=>{{calls+=1;return new Promise((_resolve,reject)=>rejectWatch=reject);}}, onState:state=>states.push(state), }}); await preview.open(detail); const first=preview.setWatching(true); const second=preview.setWatching(true); if (calls !== 1 || first !== second) throw new Error('watch mutation was not single-flight'); rejectWatch(new Error('offline')); await Promise.allSettled([first,second]); const final=states.at(-1); process.stdout.write(JSON.stringify({{calls,status:final.status,number:final.detail.number, watching:final.detail.watching,error:final.error.message}})); }})().catch(error=>{{console.error(error);process.exit(1);}}); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "calls": 1, "status": "watch-error", "number": 42, "watching": False, "error": "offline", } def test_search_preview_preserves_authoritative_watch_and_offers_following_repair(): script = f""" const createSearchPreview = require({json.dumps(str(SEARCH_PREVIEW))}); (async () => {{ const states = []; const detail = {{repository:'stackchain/api',number:42,kind:'issue',state:'open',claimable:true,watching:false}}; const preview = createSearchPreview({{ fetchJson:async()=>detail, watch:async()=>({{watching:true,following_synced:false,error:'Watching in Gitea, but Following could not sync. Retry this action.'}}), onState:state=>states.push(state), }}); await preview.open(detail); await preview.setWatching(true); const final=states.at(-1); process.stdout.write(JSON.stringify({{ status:final.status, watching:final.detail.watching, message:globalThis.searchPreviewWatchStatus(final), }})); }})().catch(error=>{{console.error(error);process.exit(1);}}); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "status": "watch-partial", "watching": True, "message": "Watching in Gitea, but Following could not sync. Tap Stop watching, then Watch issue to repair.", } def test_mobile_search_preview_exposes_touch_safe_watch_action(): html = dashboard_bundle_text() css = (FRONTEND / "dashboard.css").read_text() dashboard = (FRONTEND / "dashboard.js").read_text() assert '' in html assert "wireSearchPreviewWatch" in dashboard assert "preview.setWatching" in SEARCH_PREVIEW.read_text() assert ".search-preview-actions button" in css and "min-height:44px" in css def test_search_preview_shares_canonical_url_without_closing_the_preview(): script = f""" const createSearchPreview = require({json.dumps(str(SEARCH_PREVIEW))}); (async () => {{ const calls = []; const states = []; const preview = createSearchPreview({{ fetchJson: item => Promise.resolve(item), mutate: () => Promise.resolve(), share: async url => {{ calls.push(url); return 'shared'; }}, onState: state => states.push(state), }}); const detail = {{ repository:'stackchain/api', number:42, kind:'issue' }}; await preview.open(detail); const result = await preview.share('https://forge.example/dashboard/?search=release&preview=issue%3Astackchain%2Fapi%3A42'); process.stdout.write(JSON.stringify({{ result, calls, states }})); }})().catch(error => {{ console.error(error); process.exit(1); }}); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr payload = json.loads(result.stdout) assert payload["result"] == "shared" assert payload["calls"] == [ "https://forge.example/dashboard/?search=release&preview=issue%3Astackchain%2Fapi%3A42" ] assert payload["states"][-1]["status"] == "shared" assert payload["states"][-1]["detail"]["number"] == 42 def test_search_preview_share_is_single_flight_and_cancellation_restores_ready_context(): script = f""" const createSearchPreview = require({json.dumps(str(SEARCH_PREVIEW))}); (async () => {{ let calls = 0; let rejectShare; const states = []; const preview = createSearchPreview({{ fetchJson: item => Promise.resolve(item), mutate: () => Promise.resolve(), share: () => {{ calls += 1; return new Promise((_resolve, reject) => rejectShare = reject); }}, onState: state => states.push(state), }}); await preview.open({{ repository:'stackchain/api', number:42, kind:'issue' }}); const first = preview.share('https://forge.example/dashboard/?search=x'); const second = preview.share('https://forge.example/dashboard/?search=x'); const canceled = new Error('canceled'); canceled.name = 'AbortError'; rejectShare(canceled); await Promise.allSettled([first, second]); const state = states.at(-1); process.stdout.write(JSON.stringify({{ calls, same:first === second, status:state.status, item:state.item, detail:state.detail, errorName:state.error?.name }})); }})().catch(error => {{ console.error(error); process.exit(1); }}); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "calls": 1, "same": True, "status": "share-canceled", "item": {"repository": "stackchain/api", "number": 42, "kind": "issue"}, "detail": {"repository": "stackchain/api", "number": 42, "kind": "issue"}, "errorName": "AbortError", } def test_remote_search_selection_opens_native_preview_without_navigation(): html = dashboard_bundle_text() run_item = html.split("function runCommandItem(item)", 1)[1].split( "function renderCommands", 1 )[0] assert "searchPreview.open(item.result)" in run_item assert "window.location.assign" not in run_item remote_branch = run_item.split("} else {", 1)[1] assert "qs('#cmd-input').value = ''" not in remote_branch def test_search_preview_share_action_uses_canonical_stackchain_url_and_keeps_context(): html = dashboard_bundle_text() assert "share: url => createWorkRoute.share(url, navigator, navigator.clipboard)" in html handler = html.split("qs('#share-search-result').addEventListener('click'", 1)[1].split( "qs('#claim-search-result')", 1 )[0] assert "canonicalSearchPreviewUrl()" in handler assert "searchPreview.share" in handler assert "closeSearchPreview" not in handler assert "taskOverlayHistory.close" not in handler def test_assigned_issue_preview_hands_off_to_existing_my_work_sheet(): html = dashboard_bundle_text() assert "claimButton.hidden = !(detail.claimable || detail.assigned_to_me)" in html assert "claimButton.textContent = detail.assigned_to_me ? 'Open in My Work' : 'Assign to me'" in html assert "openPreviewWorkInMyWork" in html def test_assigned_pull_preview_hands_off_to_existing_my_work_sheet_without_claiming(): html = dashboard_bundle_text() assert "claimButton.hidden = !(detail.claimable || detail.assigned_to_me)" in html assert "async function openPreviewWorkInMyWork(detail)" in html handoff = html.split("async function openPreviewWorkInMyWork(detail)", 1)[1].split( "function runCommandItem", 1 )[0] assert "candidate.kind === detail.kind" in handoff assert "candidate.key === detail.repository + '#' + detail.number" in handoff click_handler = html.split( "qs('#claim-search-result').addEventListener('click'", 1 )[1].split("qs('#start-search-result')", 1)[0] assert "if (claimed.claimable) await searchPreview.claim(claimed)" in click_handler assert "await openPreviewWorkInMyWork(claimed)" in click_handler def test_requested_pull_preview_opens_native_review_without_assignment_mutation(): html = dashboard_bundle_text() css = (FRONTEND / "dashboard.css").read_text() assert "startButton.hidden = !(detail.reviewable" in html assert "detail.reviewable ? 'Review now'" in html handler = html.split( "qs('#start-search-result').addEventListener('click'", 1 )[1].split("qs('#close-whiteboard')", 1)[0] assert "detail.kind = 'review'" in handler assert "openRoutedWork" in handler assert "{ replace:true }" in handler assert "searchPreview.claim" not in handler assert "fetchReviewJson" not in handler assert ".search-preview-actions button, .search-preview-actions a { min-height:44px;" in css def test_search_preview_offers_assign_and_start_for_eligible_issues(): html = dashboard_bundle_text() css = (FRONTEND / "dashboard.css").read_text() assert 'id="start-search-result"' in html assert "(detail.assigned_to_me ? 'Start in Today' : 'Assign & start')" in html assert "const searchAssignAndStart = createSearchStart(detail => searchPreview.claim(detail))" in html assert "searchAssignAndStart.run(detail, { alreadyOwned: detail.assigned_to_me })" in html assert ".search-preview-primary-actions" in css assert "grid-template-columns:repeat(2,minmax(0,1fr))" in css def test_mobile_search_pauses_today_and_offers_a_lossless_return(): html = dashboard_bundle_text() css = (FRONTEND / "dashboard.css").read_text() assert html.count('