import json import subprocess from pathlib import Path ROOT = Path(__file__).parents[1] MODULE = ROOT / "frontend" / "following.js" def run(script: str) -> dict: harness = f""" const createFollowing = require({json.dumps(str(MODULE))}); const state = {{ renders:[], counts:[], opened:[], requests:[] }}; const feature = createFollowing({{ fetchJson: async path => {{ state.requests.push(path); return {{revision:3,items:[{{repository:'stackchain/api',number:42,title:'Quiet issue',state:'open',updated_at:'2026-08-23T03:00:00Z',url:'https://forge.example/issue/42',has_unseen_change:false}}]}}; }}, render: snapshot => state.renders.push(snapshot), onCount: count => state.counts.push(count), onOpen: item => state.opened.push(item), }}); (async () => {{ {script} }})().catch(error => {{ console.error(error); process.exit(1); }}); """ completed = subprocess.run(["node", "-e", harness], text=True, capture_output=True, check=True) return json.loads(completed.stdout) def test_following_loads_account_collection_and_opens_existing_preview(): result = run(""" await feature.load(); feature.open(0); process.stdout.write(JSON.stringify(state)); """) assert result["requests"] == ["api/v1/following"] assert result["counts"] == [0] assert result["renders"][-1]["status"] == "ready" assert result["renders"][-1]["items"][0]["title"] == "Quiet issue" assert result["opened"] == [{ "repository": "stackchain/api", "number": 42, "title": "Quiet issue", "state": "open", "updated_at": "2026-08-23T03:00:00Z", "url": "https://forge.example/issue/42", "has_unseen_change": False, "kind": "issue", "following": True, }] def test_following_opens_pull_requests_in_the_pull_workspace(): script = f""" const createFollowing = require({json.dumps(str(MODULE))}); const opened=[]; const feature=createFollowing({{ fetchJson:async () => ({{revision:1,items:[{{ repository:'stackchain/api',kind:'pull',number:84,title:'Typed review',state:'open', updated_at:'2026-08-23T05:00:00Z',url:'https://forge.example/pulls/84',has_unseen_change:true }}]}}), onOpen:async item => opened.push(item), onAcknowledge:async () => {{}}, }}); (async () => {{ await feature.load(); await feature.startReview(); process.stdout.write(JSON.stringify({{opened,session:feature.session()}})); }})().catch(error => {{console.error(error);process.exit(1);}}); """ result = json.loads(subprocess.run( ["node", "-e", script], text=True, capture_output=True, check=True ).stdout) assert result["opened"][0]["kind"] == "pull" assert result["opened"][0]["following"] is True assert result["session"]["items"][0]["kind"] == "pull" def test_following_route_loads_authoritative_state_before_starting_review(): following = MODULE.read_text() start = following.index("async route()") route = following[start:following.index("open()", start)] assert "await feature.load().then(feature.startReview)" in route assert ".catch(() => false)" in route assert "show();" in route def test_dashboard_dispatches_following_route_to_changed_first_review(): dashboard = (ROOT / "frontend" / "dashboard.js").read_text() start = dashboard.index("async function openWorkQueueRoute") route_handler = dashboard[start:dashboard.index("qs('#work-milestone-filter')", start)] assert "if (filter === 'following')" in route_handler assert "return followingQueue.route();" in route_handler assert route_handler.index("followingQueue.route") < route_handler.index("selectWorkQueue") def test_following_preview_wires_keep_for_later_action_and_retryable_failure(): html = (ROOT / "frontend" / "index.html").read_text() dashboard = (ROOT / "frontend" / "dashboard.js").read_text() following = MODULE.read_text() assert 'id="keep-following-for-later"' in html assert 'id="keep-following-status"' in html assert "followingQueue.preview(state)" in dashboard assert "query('#close-search-preview').click()" in following assert "state?.item?.following !== true" in following assert "feature.keepForLater(activeReviewItem)" in following assert "Kept for later" in following assert "Could not keep this change for later" in following assert "changeRevision(item, 'keep')" in following assert "keepForLater:feature.keepForLater" in following def test_following_conversation_marks_messages_newer_than_last_review(): preview_module = ROOT / "frontend" / "search-preview.js" script = f""" require({json.dumps(str(preview_module))}); const nodes={{ '#search-preview-comments':{{innerHTML:'',textContent:''}}, '#search-preview-conversation-status':{{textContent:''}}, '#retry-search-preview-conversation':{{hidden:true}}, '#load-older-search-preview-comments':{{hidden:true,disabled:false}}, }}; const document={{querySelector:selector=>nodes[selector]}}; global.renderSearchPreviewConversation({{ status:'ready', reviewedAt:'2026-08-23T03:00:00Z', comments:[ {{id:1,author:'Timmy',created_at:'2026-08-23T02:00:00Z',body:'Earlier'}}, {{id:2,author:'Alex',created_at:'2026-08-23T04:00:00Z',body:'New decision'}}, ] }},document,String,String,String); process.stdout.write(JSON.stringify({{html:nodes['#search-preview-comments'].innerHTML,status:nodes['#search-preview-conversation-status'].textContent}})); """ result = json.loads(subprocess.run( ["node", "-e", script], text=True, capture_output=True, check=True ).stdout) assert result["html"].count("new-since-review") == 1 assert "New since last review" in result["html"] assert result["status"] == "2 messages ยท 1 new since last review." def test_following_revision_is_acknowledged_only_after_conversation_context_loads(): preview_module = ROOT / "frontend" / "search-preview.js" script = f""" const createSearchPreview=require({json.dumps(str(preview_module))}); let unavailable=true; const opened=[]; const preview=createSearchPreview({{ fetchJson:async item=>({{...item,title:'Changed issue'}}), fetchConversation:async()=>{{if(unavailable)throw new Error('offline');return {{comments:[]}};}}, onOpened:async item=>opened.push(item.updated_at), onState:()=>{{}}, }}); (async()=>{{ await preview.open({{ repository:'stackchain/api',kind:'issue',number:42,following:true, reviewed_at:'2026-08-23T03:00:00Z',updated_at:'2026-08-23T04:00:00Z' }}); const afterFailure=[...opened]; unavailable=false; await preview.retryConversation(); process.stdout.write(JSON.stringify({{afterFailure,opened}})); }})().catch(error=>{{console.error(error);process.exit(1);}}); """ result = json.loads(subprocess.run( ["node", "-e", script], text=True, capture_output=True, check=True ).stdout) assert result == { "afterFailure": [], "opened": ["2026-08-23T04:00:00Z"], } def test_following_typed_identity_prevents_issue_pull_collisions(): script = f""" const createFollowing = require({json.dumps(str(MODULE))}); const acknowledged=[]; const items=['issue','pull'].map(kind => ({{ repository:'stackchain/api',kind,number:84,title:kind,state:'closed', updated_at:'2026-08-23T05:00:00Z',has_unseen_change:true }})); const feature=createFollowing({{ fetchJson:async () => ({{revision:1,items}}), onOpen:async () => {{}}, onAcknowledge:async item => acknowledged.push(item.kind), }}); (async () => {{ await feature.load(); await feature.previewLoaded({{...items[1]}}); feature.retire(items[1]); process.stdout.write(JSON.stringify({{acknowledged,items:feature.items()}})); }})().catch(error => {{console.error(error);process.exit(1);}}); """ result = json.loads(subprocess.run( ["node", "-e", script], text=True, capture_output=True, check=True ).stdout) assert result["acknowledged"] == ["pull"] assert [(item["kind"], item["has_unseen_change"]) for item in result["items"]] == [ ("issue", True) ] def test_following_opens_explicitly_but_never_becomes_work_recommendation(): launcher = ROOT / "frontend" / "mobile-queue-launcher.js" script = f""" const createLauncher = require({json.dumps(str(launcher))}); const calls = []; const feature = createLauncher({{ getCounts:() => ({{following:7}}), openFollowing:() => {{ calls.push('following'); return 'opened-following'; }}, selectFilter:name => calls.push(name), firstAction:() => null, announce:() => {{}}, openFindWork:() => calls.push('find'), }}); process.stdout.write(JSON.stringify({{opened:feature.open('following'),recommended:feature.recommend(),calls}})); """ result = json.loads(subprocess.run( ["node", "-e", script], text=True, capture_output=True, check=True ).stdout) assert result == { "opened": "opened-following", "recommended": {"name": "find", "count": 0, "label": "Find Work"}, "calls": ["following"], } def test_following_counts_unseen_changes_and_acknowledges_after_preview_loads(): script = f""" const createFollowing = require({json.dumps(str(MODULE))}); const state = {{counts:[], opened:[], acknowledged:[], renders:[]}}; let finishPreview; const feature = createFollowing({{ fetchJson:async () => ({{revision:4,items:[ {{repository:'stackchain/api',number:42,title:'Changed',state:'open',updated_at:'2026-08-23T04:00:00Z',url:'https://forge/42',has_unseen_change:true}}, {{repository:'stackchain/api',number:43,title:'Quiet',state:'open',updated_at:'2026-08-23T03:00:00Z',url:'https://forge/43',has_unseen_change:false}} ]}}), render:value => state.renders.push(value), onCount:value => state.counts.push(value), onOpen:item => new Promise(resolve => {{ finishPreview=() => {{ state.opened.push(item.number); resolve(); }}; }}), onAcknowledge:async item => state.acknowledged.push(item.updated_at), }}); (async () => {{ await feature.load(); const opening=feature.open(0); await new Promise(resolve => setImmediate(resolve)); state.before={{acknowledged:[...state.acknowledged],counts:[...state.counts]}}; finishPreview(); await opening; process.stdout.write(JSON.stringify(state)); }})().catch(error => {{ console.error(error); process.exit(1); }}); """ result = json.loads(subprocess.run( ["node", "-e", script], text=True, capture_output=True, check=True ).stdout) assert result["before"] == {"acknowledged": [], "counts": [1]} assert result["opened"] == [42] assert result["acknowledged"] == ["2026-08-23T04:00:00Z"] assert result["counts"] == [1, 0] assert result["renders"][-1]["items"][0]["has_unseen_change"] is False def test_following_review_captures_only_current_unseen_changes_in_display_order(): script = f""" const createFollowing = require({json.dumps(str(MODULE))}); const state = {{opened:[], acknowledged:[]}}; const feature = createFollowing({{ fetchJson:async () => ({{revision:7,items:[ {{repository:'stackchain/api',number:42,title:'Newest',state:'open',updated_at:'2026-08-23T06:00:00Z',has_unseen_change:true}}, {{repository:'stackchain/web',number:9,title:'Also changed',state:'open',updated_at:'2026-08-23T05:00:00Z',has_unseen_change:true}}, {{repository:'stackchain/api',number:41,title:'Quiet',state:'open',updated_at:'2026-08-22T05:00:00Z',has_unseen_change:false}} ]}}), onOpen:async item => state.opened.push(item.number), onAcknowledge:async item => state.acknowledged.push(item.number), }}); (async () => {{ await feature.load(); const started=await feature.startReview(); process.stdout.write(JSON.stringify({{started,session:feature.session(),state}})); }})().catch(error => {{ console.error(error); process.exit(1); }}); """ result = json.loads(subprocess.run( ["node", "-e", script], text=True, capture_output=True, check=True ).stdout) assert result["started"] is True assert [item["number"] for item in result["session"]["items"]] == [42, 9] assert all(item["kind"] == "issue" for item in result["session"]["items"]) assert all(item["following"] is True for item in result["session"]["items"]) assert result["session"]["more"] is False assert result["state"] == {"opened": [42], "acknowledged": [42]} def test_following_review_acknowledges_each_loaded_revision_and_reports_completion(): script = f""" const createFollowing = require({json.dumps(str(MODULE))}); const state = {{acknowledged:[], renders:[], counts:[]}}; const items=[ {{repository:'stackchain/api',number:42,title:'First',updated_at:'2026-08-23T06:00:00Z',has_unseen_change:true}}, {{repository:'stackchain/web',number:9,title:'Second',updated_at:'2026-08-23T05:00:00Z',has_unseen_change:true}} ]; const feature = createFollowing({{ fetchJson:async () => ({{revision:7,items}}), render:value => state.renders.push(value), onCount:value => state.counts.push(value), onOpen:async () => {{}}, onAcknowledge:async item => state.acknowledged.push(item.number), }}); (async () => {{ await feature.load(); await feature.startReview(); await feature.previewLoaded(feature.session().items[1]); process.stdout.write(JSON.stringify(state)); }})().catch(error => {{ console.error(error); process.exit(1); }}); """ result = json.loads(subprocess.run( ["node", "-e", script], text=True, capture_output=True, check=True ).stdout) assert result["acknowledged"] == [42, 9] assert result["counts"][-1] == 0 assert result["renders"][-1]["reviewSummary"] == {"reviewed": 2, "remaining": 0} def test_following_keep_for_later_preserves_revision_and_advances_without_looping(): script = f""" const createFollowing = require({json.dumps(str(MODULE))}); const state = {{opened:[], acknowledged:[], kept:[], counts:[]}}; const items=[ {{repository:'stackchain/api',number:42,title:'First',updated_at:'2026-08-23T06:00:00Z',has_unseen_change:true}}, {{repository:'stackchain/web',number:9,title:'Second',updated_at:'2026-08-23T05:00:00Z',has_unseen_change:true}} ]; const feature = createFollowing({{ fetchJson:async () => ({{revision:7,items}}), onOpen:async item => state.opened.push(item.number), onAcknowledge:async item => state.acknowledged.push(item.number), onKeep:async item => state.kept.push(item.number), onCount:value => state.counts.push(value), }}); (async () => {{ await feature.load(); await feature.startReview(); const kept = await feature.keepForLater(feature.session().items[0]); process.stdout.write(JSON.stringify({{ kept, session:feature.session(), items:feature.items(), state }})); }})().catch(error => {{ console.error(error); process.exit(1); }}); """ result = json.loads(subprocess.run( ["node", "-e", script], text=True, capture_output=True, check=True ).stdout) assert result["kept"]["number"] == 9 assert [item["number"] for item in result["session"]["items"]] == [9] assert result["items"][0]["has_unseen_change"] is True assert result["state"] == { "opened": [42, 9], "acknowledged": [42, 9], "kept": [42], "counts": [2, 1, 2, 1] } def test_following_review_notifies_prepare_today_when_final_revision_is_loaded(): script = f""" const createFollowing = require({json.dumps(str(MODULE))}); const completed = []; const items=[ {{repository:'stackchain/api',number:42,title:'First',updated_at:'2026-08-23T06:00:00Z',has_unseen_change:true}}, {{repository:'stackchain/web',number:9,title:'Second',updated_at:'2026-08-23T05:00:00Z',has_unseen_change:true}} ]; const feature = createFollowing({{ fetchJson:async () => ({{revision:7,items}}), onOpen:async () => {{}}, onAcknowledge:async () => {{}}, onReviewComplete:() => completed.push('following'), }}); (async () => {{ await feature.load(); await feature.startReview(); await feature.previewLoaded(feature.session().items[1]); const beforeFinish = completed.slice(); feature.finishReview(); feature.finishReview(); process.stdout.write(JSON.stringify({{beforeFinish, completed}})); }})().catch(error => {{ console.error(error); process.exit(1); }}); """ assert json.loads(subprocess.run( ["node", "-e", script], text=True, capture_output=True, check=True ).stdout) == {"beforeFinish": [], "completed": ["following"]} def test_following_retirement_removes_closed_item_and_keeps_review_moving(): script = f""" const createFollowing = require({json.dumps(str(MODULE))}); const state = {{renders:[], counts:[], completed:[]}}; const items=[ {{repository:'stackchain/api',number:42,title:'Closed first',state:'closed',updated_at:'2026-08-23T06:00:00Z',has_unseen_change:true}}, {{repository:'stackchain/web',number:9,title:'Closed second',state:'closed',updated_at:'2026-08-23T05:00:00Z',has_unseen_change:true}} ]; const feature = createFollowing({{ fetchJson:async () => ({{revision:7,items}}), render:value => state.renders.push(value), onCount:value => state.counts.push(value), onOpen:async () => {{}}, onAcknowledge:async () => {{}}, onReviewComplete:() => state.completed.push('following'), }}); (async () => {{ await feature.load(); await feature.startReview(); const first=feature.session().items[0]; const next=feature.retire(first); await feature.previewLoaded(next); const final=feature.retire(next); const handoff=feature.finishReview(); process.stdout.write(JSON.stringify({{ next:next.number, final, handoff, items:feature.items(), session:feature.session(), state }})); }})().catch(error => {{ console.error(error); process.exit(1); }}); """ result = json.loads(subprocess.run( ["node", "-e", script], text=True, capture_output=True, check=True ).stdout) assert result["next"] == 9 assert result["final"] is None assert result["handoff"] is True assert result["items"] == [] assert result["session"] is None assert result["state"]["counts"][-1] == 0 assert result["state"]["completed"] == ["following"] def test_following_review_never_clears_activity_newer_than_the_loaded_preview(): script = f""" const createFollowing = require({json.dumps(str(MODULE))}); const state = {{acknowledged:[], counts:[]}}; let loads=0; const original=[ {{repository:'stackchain/api',number:42,title:'First',updated_at:'2026-08-23T06:00:00Z',has_unseen_change:true}}, {{repository:'stackchain/web',number:9,title:'Second',updated_at:'2026-08-23T05:00:00Z',has_unseen_change:true}} ]; const refreshed=[ {{...original[0],has_unseen_change:false}}, {{...original[1],updated_at:'2026-08-23T07:00:00Z',has_unseen_change:true}} ]; const feature = createFollowing({{ fetchJson:async () => ({{revision:++loads,items:loads === 1 ? original : refreshed}}), onCount:value => state.counts.push(value), onOpen:async () => {{}}, onAcknowledge:async item => state.acknowledged.push(item.updated_at), }}); (async () => {{ await feature.load(); await feature.startReview(); const captured=feature.session().items[1]; await feature.load(); const cleared=await feature.previewLoaded(captured); process.stdout.write(JSON.stringify({{cleared,state}})); }})().catch(error => {{ console.error(error); process.exit(1); }}); """ result = json.loads(subprocess.run( ["node", "-e", script], text=True, capture_output=True, check=True ).stdout) assert result == { "cleared": False, "state": { "acknowledged": ["2026-08-23T06:00:00Z"], "counts": [2, 1, 1], }, } def test_following_review_controls_are_wired_into_the_phone_preview_flow(): html = (ROOT / "frontend" / "index.html").read_text() css = (ROOT / "frontend" / "dashboard.css").read_text() following = MODULE.read_text() dashboard = (ROOT / "frontend" / "dashboard.js").read_text() service_worker = (ROOT / "frontend" / "service-worker.js").read_text() assert 'id="review-following"' in html assert '>Review new activity' in html assert ".following-review-action" in css assert "min-height:44px" in css assert "query('#review-following').addEventListener('click'" in following assert "getSession:() => followingQueue.session() || commandSearchState" in dashboard assert "onOpened:item => followingQueue.previewLoaded(item)" in dashboard assert "changeRevision(item, 'seen')" in following assert "afterUnwatch:item => followingQueue.retire(item)" in dashboard assert "followingQueue.returnToFollowing()" in dashboard assert "'Back to Following'" in dashboard assert "if (searchPreviewReturnKind === 'following')" in dashboard assert "e.key === 'Escape' && searchPreviewReturnKind === 'following'" in dashboard assert "stackchain-dashboard-shell-v136" in service_worker def test_prepare_today_lazily_refreshes_and_directly_reviews_following(): following = MODULE.read_text() dashboard = (ROOT / "frontend" / "dashboard.js").read_text() assert "function attachFollowing(onOpen, hooks = {})" in following assert "hooks.onCount?.(count, feature.items())" in following assert "hooks.onStatus?.(state.status)" in following assert "onReviewComplete:hooks.onReviewComplete" in following assert "review:feature.startReview" in following assert "const completed = feature.finishReview()" in following assert "if (completed) return 'completed-following'" in following assert "followingQueue.load().catch(() => {})" in dashboard assert "mobileQueueCounts.following = count" in dashboard assert "mobileQueueCounts.followingUnavailable = status === 'error'" in dashboard assert "mobilePreparationItems.following = items.filter" in dashboard assert "mobileQueueCounts.followingUnavailable ? followingQueue.open()" in dashboard assert "mobileStartDay.completePhase('following')" in dashboard