feat: create follow-up work from updates (Closes #663)
This commit is contained in:
1 parent
97eb928187
commit
d96a77c5d5
7 files changed
+210
-1
No files matched your search
@@ -711,6 +711,7 @@ def test_install_precaches_complete_subpath_scoped_app_shell():
|
||||
"/dashboard/static/today-sync.js",
|
||||
"/dashboard/static/today-rollover.js",
|
||||
"/dashboard/static/update-ownership.js",
|
||||
"/dashboard/static/update-follow-up.js",
|
||||
"/dashboard/static/later-work.js",
|
||||
"/dashboard/static/later-sync.js",
|
||||
"/dashboard/static/later-and-start.js",
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.dashboard_bundle import dashboard
|
||||
|
||||
|
||||
ROOT = Path(__file__).parents[1]
|
||||
UPDATE_FOLLOW_UP = ROOT / "frontend" / "update-follow-up.js"
|
||||
CREATE_ISSUE_SHEET = ROOT / "frontend" / "create-issue-sheet.js"
|
||||
|
||||
|
||||
def run_node(script: str) -> dict:
|
||||
result = subprocess.run(
|
||||
["node", "-e", script], capture_output=True, text=True, timeout=10
|
||||
)
|
||||
assert result.returncode == 0, result.stderr
|
||||
return json.loads(result.stdout)
|
||||
|
||||
|
||||
def test_follow_up_derives_repository_editable_title_and_canonical_context():
|
||||
script = f"""
|
||||
const createFollowUp = require({json.dumps(str(UPDATE_FOLLOW_UP))});
|
||||
const controller = createFollowUp();
|
||||
process.stdout.write(JSON.stringify(controller.draft({{
|
||||
repository: 'stackchain/stackchain-dashboard',
|
||||
title: 'Fix mobile queue',
|
||||
subject_type: 'Pull',
|
||||
url: 'https://forge.example/git/stackchain/stackchain-dashboard/pulls/42',
|
||||
latest_comment: {{ body: 'Please preserve the unread state. Ship this on mobile.' }}
|
||||
}})));
|
||||
"""
|
||||
assert run_node(script) == {
|
||||
"repository": "stackchain/stackchain-dashboard",
|
||||
"title": "Follow up: Fix mobile queue",
|
||||
"body": (
|
||||
"Source: https://forge.example/git/stackchain/stackchain-dashboard/pulls/42\n\n"
|
||||
"Latest context:\n> Please preserve the unread state. Ship this on mobile."
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def test_follow_up_rejects_unsafe_repository_and_noncanonical_source_url():
|
||||
script = f"""
|
||||
const createFollowUp = require({json.dumps(str(UPDATE_FOLLOW_UP))});
|
||||
const controller = createFollowUp();
|
||||
process.stdout.write(JSON.stringify(controller.draft({{
|
||||
repository: '../admin', title: '<script>alert(1)</script>',
|
||||
url: 'javascript:alert(1)', latest_comment: {{body: 'x'.repeat(12000)}}
|
||||
}})));
|
||||
"""
|
||||
output = run_node(script)
|
||||
assert output["repository"] == ""
|
||||
assert output["title"] == "Follow up: <script>alert(1)</script>"
|
||||
assert "javascript:" not in output["body"]
|
||||
assert len(output["body"]) <= 9500
|
||||
|
||||
|
||||
def test_follow_up_staging_never_silently_overwrites_an_existing_capture():
|
||||
script = f"""
|
||||
const createCapture = require({json.dumps(str(CREATE_ISSUE_SHEET))});
|
||||
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 capture = createCapture({{fetchJson:async()=>[], storage}});
|
||||
capture.saveDraft({{repository:'o/existing', title:'Existing draft', body:'Keep me'}});
|
||||
const state = capture.stageFollowUp({{repository:'o/new', title:'Follow up', body:'Source: https://example.test/1'}});
|
||||
const before = capture.loadDraft();
|
||||
const accepted = capture.acceptFollowUp();
|
||||
process.stdout.write(JSON.stringify({{state, before, accepted, pending:capture.pendingFollowUp()}}));
|
||||
"""
|
||||
assert run_node(script) == {
|
||||
"state": {"status": "conflict"},
|
||||
"before": {
|
||||
"repository": "o/existing",
|
||||
"title": "Existing draft",
|
||||
"body": "Keep me",
|
||||
"labelIds": [],
|
||||
},
|
||||
"accepted": {
|
||||
"repository": "o/new",
|
||||
"title": "Follow up",
|
||||
"body": "Source: https://example.test/1",
|
||||
"labelIds": [],
|
||||
},
|
||||
"pending": None,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_update_sheet_wires_phone_safe_follow_up_without_marking_read():
|
||||
html = await dashboard()
|
||||
|
||||
assert '<script src="static/update-follow-up.js"></script>' in html
|
||||
assert 'id="create-update-follow-up"' in html
|
||||
assert '>Create follow-up</button>' in html
|
||||
assert "issueCapture.stageFollowUp(updateFollowUp.draft(selectedUpdateDetail))" in html
|
||||
assert "qs('#create-update-follow-up').addEventListener('click'" in html
|
||||
assert "markNotificationRead" not in html.split("qs('#create-update-follow-up').addEventListener('click'", 1)[1].split("});", 1)[0]
|
||||
assert '.update-sheet-actions button, .update-sheet-actions a { min-height:44px;' in html
|
||||
Reference in new issue
Block a user