intent/tests/test_example_flow.py
Alec Perkins 5811e72a0c Add issues and comment threads (Phase 3)
Issues: per-project numbers, statuses (open, accepted, rejected,
withdrawn, duplicate), feature links, edits, and a timeline of comments
and events. From an issue, collaborators create or update a feature
with the revision tied to the issue and an option to accept it.

Comments: general and line-anchored threads on features, one
discussion thread per issue, edit history, tombstone deletes, resolve,
and promoting a feature thread to an issue. Line threads attach to
top-level Markdown blocks and go outdated only when the text changes.

HTMX swaps threads in place with focus management; every action also
works as a plain form post. Markdown headings are re-levelled so user
text never skips heading levels on the page.

The example flow from concept.md (steps 1-7) is covered end to end in
tests/test_example_flow.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 09:18:45 -04:00

136 lines
5.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""The example flow from docs/concept.md, steps 1–7, driven through the views like a browser."""
from django.urls import reverse
from intent.comments.models import CommentThread
from intent.features.models import Feature
from intent.issues.models import Issue, IssueStatus
P = {"org_slug": "catbriar", "project_slug": "intent"}
def issue_url(name, number, **kwargs):
return reverse(f"issues:{name}", kwargs={**P, "number": number, **kwargs})
def feature_url(name, key, **kwargs):
return reverse(f"features:{name}", kwargs={**P, "feature_key": key, **kwargs})
def reply_url(thread):
return reverse("comments:reply", kwargs={**P, "thread_id": thread.pk})
def test_example_flow(client, open_project, outsider, editor):
# 1. A general user creates a new issue in a project.
client.force_login(outsider)
client.post(
reverse("issues:create", kwargs=P),
{"title": "Remember me on shared computers", "body": "Sessions last too long."},
)
issue = Issue.objects.get()
# 2. Users, including team members, discuss in comments on the issue.
issue_thread = issue.threads.get()
client.post(reply_url(issue_thread), {"body": "Public library computers especially."})
client.force_login(editor)
client.post(reply_url(issue_thread), {"body": "Agreed, let's define this."})
# 3. A team member accepts the issue and creates a feature from it, linked to the issue.
create = reverse("features:create", kwargs=P) + f"?issue={issue.number}"
client.post(
create,
{
"title": "Session length",
"description": "Sessions end after 12 hours.\n\nShared computers end at close.\n",
"note": "",
"accept_issue": "on",
},
)
issue.refresh_from_db()
feature = Feature.objects.get()
assert issue.status == IssueStatus.ACCEPTED
assert list(issue.features.all()) == [feature]
assert feature.revisions.get().issues.get() == issue
# 4. The team member updates the feature's status manually.
client.post(
feature_url("edit", feature.key),
{
"title": feature.title,
"description": feature.description,
"status": "withdrawn",
"note": "Paused until the login rework.",
},
)
feature.refresh_from_db()
assert (feature.status, feature.current_revision) == ("withdrawn", 2)
client.post(
feature_url("edit", feature.key),
{
"title": feature.title,
"description": feature.description,
"status": "active",
"note": "Back on.",
},
)
# 5. Later, a user starts a comment thread on lines of the feature.
client.force_login(outsider)
client.post(
feature_url("thread_create", feature.key),
{"body": "What counts as close?", "revision": 3, "line_start": 3, "line_end": 3},
)
thread = CommentThread.objects.get(feature=feature)
# 6. After some discussion, the user promotes the thread to an issue.
client.force_login(editor)
client.post(reply_url(thread), {"body": "Browser close, I think."})
client.force_login(outsider)
promote = reverse("comments:promote", kwargs={**P, "thread_id": thread.pk})
client.post(promote, {"title": "Define 'close'", "body": "From the thread."})
follow_up = Issue.objects.get(number=2)
thread.refresh_from_db()
assert thread.promoted_to == follow_up and thread.is_resolved
assert list(follow_up.features.all()) == [feature]
# 7. More discussion, then a team member accepts the issue and updates the feature.
client.post(reply_url(follow_up.threads.get()), {"body": "Tab close too?"})
client.force_login(editor)
client.post(
feature_url("edit", feature.key) + f"?issue={follow_up.number}",
{
"title": feature.title,
"description": (
"Sessions end after 12 hours.\n\nShared computers end when the browser closes.\n"
),
"status": "active",
"note": "Define close as browser close.",
"accept_issue": "on",
},
)
follow_up.refresh_from_db()
feature.refresh_from_db()
assert follow_up.status == IssueStatus.ACCEPTED
assert feature.current_revision == 4
assert feature.revisions.get(revision=4).issues.get() == follow_up
# The line thread is now outdated, and the feature page shows both issues.
page = client.get(feature_url("detail", feature.key)).content.decode()
assert "Comments on earlier revisions" in page
assert "#1 Remember me on shared computers" in page and "#2 Define" in page
# 7, alternative: a third issue is rejected but stays linked, so the proposal is visible.
client.force_login(outsider)
client.post(
reverse("issues:create", kwargs=P),
{"title": "Never expire sessions", "features": [feature.pk]},
)
client.force_login(editor)
client.post(issue_url("status", 3), {"status": "rejected"})
rejected = Issue.objects.get(number=3)
assert rejected.status == IssueStatus.REJECTED
assert (
"#3 Never expire sessions"
in client.get(feature_url("detail", feature.key)).content.decode()
)