616 lines
25 KiB
Python
616 lines
25 KiB
Python
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_pull_revision_waits_for_changes_and_ci_context():
|
|
preview_module = ROOT / "frontend" / "search-preview.js"
|
|
script = f"""
|
|
const createSearchPreview=require({json.dumps(str(preview_module))});
|
|
let unavailable=true;
|
|
const opened=[];
|
|
const states=[];
|
|
const preview=createSearchPreview({{
|
|
fetchJson:async item=>({{...item,title:'Changed pull request'}}),
|
|
fetchConversation:async()=>({{comments:[]}}),
|
|
fetchReview:async()=>{{if(unavailable)throw new Error('diff offline');return {{ci_state:'success',files:[{{filename:'src/api.py',diff_lines:['+safe'],diff_available:true}}]}};}},
|
|
onOpened:async item=>opened.push(item.updated_at),
|
|
onState:state=>states.push({{status:state.review?.status,ci:state.review?.data?.ci_state}}),
|
|
}});
|
|
(async()=>{{
|
|
await preview.open({{
|
|
repository:'stackchain/api',kind:'pull',number:84,following:true,
|
|
reviewed_at:'2026-08-23T03:00:00Z',updated_at:'2026-08-23T04:00:00Z'
|
|
}});
|
|
const afterFailure=[...opened];
|
|
unavailable=false;
|
|
await preview.retryReview();
|
|
process.stdout.write(JSON.stringify({{afterFailure,opened,last:states.at(-1)}}));
|
|
}})().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"],
|
|
"last": {"status": "ready", "ci": "success"},
|
|
}
|
|
|
|
|
|
def test_watched_pull_changes_render_bounded_diff_and_ci_state():
|
|
preview_module = ROOT / "frontend" / "search-preview.js"
|
|
script = f"""
|
|
require({json.dumps(str(preview_module))});
|
|
const nodes={{
|
|
'#search-preview-review':{{hidden:true}},
|
|
'#search-preview-review-status':{{textContent:''}},
|
|
'#search-preview-files':{{innerHTML:''}},
|
|
'#retry-search-preview-review':{{hidden:true}},
|
|
}};
|
|
const document={{querySelector:selector=>nodes[selector]}};
|
|
global.renderSearchPreviewReview({{status:'ready',data:{{ci_state:'success',files:[{{
|
|
filename:'src/api.py',status:'modified',additions:1,deletions:0,
|
|
diff_available:true,diff_lines:['@@ -1 +1 @@','-unsafe','+safe']
|
|
}}]}}}},document,value=>String(value).replaceAll('<','<'));
|
|
process.stdout.write(JSON.stringify({{
|
|
hidden:nodes['#search-preview-review'].hidden,
|
|
status:nodes['#search-preview-review-status'].textContent,
|
|
html:nodes['#search-preview-files'].innerHTML,
|
|
retry:nodes['#retry-search-preview-review'].hidden,
|
|
}}));
|
|
"""
|
|
result = json.loads(subprocess.run(
|
|
["node", "-e", script], text=True, capture_output=True, check=True
|
|
).stdout)
|
|
|
|
assert result["hidden"] is False
|
|
assert result["status"] == "CI passed · 1 changed file."
|
|
assert "src/api.py" in result["html"]
|
|
assert "pull-diff-line added" in result["html"]
|
|
assert result["retry"] is True
|
|
|
|
|
|
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_and_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": "following", "count": 7, "label": "Review Following (7)"},
|
|
"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</button>' 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
|
|
|
|
|
|
def test_following_open_issue_exposes_only_action_and_continue_planning_dispositions():
|
|
script = f"""
|
|
const createFollowing = require({json.dumps(str(MODULE))});
|
|
const disposition = createFollowing.planningDisposition;
|
|
process.stdout.write(JSON.stringify({{
|
|
active:disposition({{following:true,kind:'issue',state:'open',claimable:true}}),
|
|
generic:disposition({{following:false,kind:'issue',state:'open',claimable:true}}),
|
|
pull:disposition({{following:true,kind:'pull',state:'open',assigned_to_me:true}}),
|
|
}}));
|
|
"""
|
|
result = json.loads(subprocess.run(
|
|
["node", "-e", script], text=True, capture_output=True, check=True
|
|
).stdout)
|
|
|
|
assert result == {
|
|
"active": {
|
|
"active": True,
|
|
"today": "Add to Today & next",
|
|
"later": "Later & next",
|
|
"week": "Week Ahead & next",
|
|
},
|
|
"generic": {"active": False},
|
|
"pull": {"active": False},
|
|
}
|
|
|
|
dashboard = (ROOT / "frontend" / "dashboard.js").read_text()
|
|
css = (ROOT / "frontend" / "dashboard.css").read_text()
|
|
service_worker = (ROOT / "frontend" / "service-worker.js").read_text()
|
|
assert "followingQueue.preview(state);" in dashboard
|
|
assert "following-disposition-mode" in MODULE.read_text()
|
|
assert "controls.claim.hidden = controls.start.hidden = controls.watch.hidden = true" in MODULE.read_text()
|
|
assert ".following-disposition-mode" in css
|
|
assert "if (searchPreviewReturnKind === 'following')" in dashboard
|
|
assert "e.key === 'Escape' && searchPreviewReturnKind === 'following'" in dashboard
|
|
assert "stackchain-dashboard-shell-v150" 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 "queueCounts.following = count" in dashboard
|
|
assert "queueCounts.followingUnavailable = status === 'error'" in dashboard
|
|
assert "preparationItems.following = items.filter" in dashboard
|
|
assert "queueCounts.followingUnavailable ? followingQueue.open()" in dashboard
|
|
assert "mobileStartDay.completePhase('following')" in dashboard
|