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_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_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 "'/seen?kind=' + encodeURIComponent(item.kind)" 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-v135" 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