feat: normalize URLs without scheme to https://

- Add normalize_url() helper function in commands.py
- Automatically prefix URLs without scheme (e.g. github.com → https://github.com)
- Applies to both -link flag and auto-detected URLs
- Add 5 new tests for URL normalization
- Fix existing tests to handle 5-value return from parse_args()

Examples:
  /add Fix bug github.com/user/repo
  → stored as: https://github.com/user/repo
This commit is contained in:
shokollm
2026-04-09 10:10:06 +00:00
parent 4885be0752
commit 2158f71fd0
2 changed files with 46 additions and 15 deletions

View File

@@ -81,6 +81,15 @@ def parse_args(
return True return True
return False return False
def normalize_url(url: str) -> str:
"""Normalize URL by adding https:// prefix if missing."""
if not url:
return url
if url.startswith("http://") or url.startswith("https://"):
return url
# Add https:// for URLs without scheme
return f"https://{url}"
def is_time(s: str) -> bool: def is_time(s: str) -> bool:
if not s or ":" not in s: if not s or ":" not in s:
return False return False
@@ -106,7 +115,7 @@ def parse_args(
if arg == "-link": if arg == "-link":
if i + 1 < len(args) and not args[i + 1].startswith("-"): if i + 1 < len(args) and not args[i + 1].startswith("-"):
link = args[i + 1] link = normalize_url(args[i + 1])
i += 2 i += 2
else: else:
clear_link = True clear_link = True
@@ -129,7 +138,7 @@ def parse_args(
clear_date = True clear_date = True
i += 1 i += 1
elif not link and is_url(arg): elif not link and is_url(arg):
link = arg link = normalize_url(arg)
i += 1 i += 1
elif due_date_ts is None: elif due_date_ts is None:
due_date_ts = parse_date_with_tz(arg) due_date_ts = parse_date_with_tz(arg)

View File

@@ -52,75 +52,97 @@ class TestExtractArgs:
class TestParseArgs: class TestParseArgs:
def test_text_only(self): def test_text_only(self):
text, link, due = parse_args(["hello", "world"]) text, link, due, _, _ = parse_args(["hello", "world"])
assert text == "hello world" assert text == "hello world"
assert link is None assert link is None
assert due is None assert due is None
def test_link_extracted(self): def test_link_extracted(self):
text, link, due = parse_args(["hello", "https://example.com"]) text, link, due, _, _ = parse_args(["hello", "https://example.com"])
# "hello" is non-link non-date → becomes text; only the URL becomes link # "hello" is non-link non-date → becomes text; only the URL becomes link
assert text == "hello" assert text == "hello"
assert link == "https://example.com" assert link == "https://example.com"
assert due is None assert due is None
def test_text_and_link(self): def test_text_and_link(self):
text, link, due = parse_args(["hello", "world", "https://example.com"]) text, link, due, _, _ = parse_args(["hello", "world", "https://example.com"])
assert text == "hello world" assert text == "hello world"
assert link == "https://example.com" assert link == "https://example.com"
def test_due_date_parsed(self): def test_due_date_parsed(self):
text, link, due = parse_args(["hello", "tomorrow"]) text, link, due, _, _ = parse_args(["hello", "tomorrow"])
assert text == "hello" assert text == "hello"
assert due is not None assert due is not None
# Should be some time in the future # Should be some time in the future
assert due > int(time.time()) assert due > int(time.time())
def test_all_three(self): def test_all_three(self):
text, link, due = parse_args(["hello", "https://example.com", "tomorrow"]) text, link, due, _, _ = parse_args(["hello", "https://example.com", "tomorrow"])
assert text == "hello" assert text == "hello"
assert link == "https://example.com" assert link == "https://example.com"
assert due is not None assert due is not None
def test_http_and_https_both_detected(self): def test_http_and_https_both_detected(self):
_, link1, _ = parse_args(["http://example.com"]) _, link1, _, _, _ = parse_args(["http://example.com"])
_, link2, _ = parse_args(["https://example.com"]) _, link2, _, _, _ = parse_args(["https://example.com"])
assert link1 == "http://example.com" assert link1 == "http://example.com"
assert link2 == "https://example.com" assert link2 == "https://example.com"
def test_non_url_non_date_becomes_text(self): def test_non_url_non_date_becomes_text(self):
text, link, due = parse_args(["fix", "the", "bug"]) text, link, due, _, _ = parse_args(["fix", "the", "bug"])
assert text == "fix the bug" assert text == "fix the bug"
assert link is None assert link is None
assert due is None assert due is None
def test_multiple_links_first_only(self): def test_multiple_links_first_only(self):
_, link, _ = parse_args(["text", "https://first.com", "https://second.com"]) _, link, _, _, _ = parse_args(["text", "https://first.com", "https://second.com"])
assert link == "https://first.com" assert link == "https://first.com"
def test_due_date_after_link(self): def test_due_date_after_link(self):
text, link, due = parse_args(["task", "https://example.com", "in 5 days"]) text, link, due, _, _ = parse_args(["task", "https://example.com", "in 5 days"])
assert text == "task" assert text == "task"
assert link == "https://example.com" assert link == "https://example.com"
assert due is not None assert due is not None
def test_empty_args(self): def test_empty_args(self):
text, link, due = parse_args([]) text, link, due, _, _ = parse_args([])
assert text is None assert text is None
assert link is None assert link is None
assert due is None assert due is None
def test_date_parser_failure_returns_none(self): def test_date_parser_failure_returns_none(self):
# "asdfjkl" is not parseable → goes to text # "asdfjkl" is not parseable → goes to text
text, link, due = parse_args(["hello", "asdfjkl"]) text, link, due, _, _ = parse_args(["hello", "asdfjkl"])
assert text == "hello asdfjkl" assert text == "hello asdfjkl"
assert due is None assert due is None
def test_link_takes_first_match(self): def test_link_takes_first_match(self):
# Even if it's not a valid URL, starts with https:// # Even if it's not a valid URL, starts with https://
_, link, _ = parse_args(["skip", "https://not-real.but-still-a-link"]) _, link, _, _, _ = parse_args(["skip", "https://not-real.but-still-a-link"])
assert link == "https://not-real.but-still-a-link" assert link == "https://not-real.but-still-a-link"
def test_url_without_scheme_normalized_to_https(self):
"""URLs without scheme should get https:// prefix."""
_, link, _, _, _ = parse_args(["github.com/user/repo"])
assert link == "https://github.com/user/repo"
def test_url_without_scheme_github_normalized(self):
text, link, _, _, _ = parse_args(["Fix bug", "github.com/owner/repo"])
assert text == "Fix bug"
assert link == "https://github.com/owner/repo"
def test_url_with_explicit_https_unchanged(self):
_, link, _, _, _ = parse_args(["task", "https://example.com/page"])
assert link == "https://example.com/page"
def test_url_with_http_unchanged(self):
_, link, _, _, _ = parse_args(["task", "http://example.com/page"])
assert link == "http://example.com/page"
def test_url_link_flag_without_scheme_normalized(self):
_, link, _, _, _ = parse_args(["-link", "example.com/path"])
assert link == "https://example.com/path"
class TestFormatBounty: class TestFormatBounty:
def _row( def _row(