diff --git a/scripts/test_yunxiao_github_sync.py b/scripts/test_yunxiao_github_sync.py
index 3cda86526..a5aac37d9 100644
--- a/scripts/test_yunxiao_github_sync.py
+++ b/scripts/test_yunxiao_github_sync.py
@@ -356,6 +356,45 @@ def test_description_source_requires_the_leading_source_line(self):
self.assertIsNone(MODULE.description_source_url(f"Body mentions\nGitHub: {url}"))
self.assertIsNone(MODULE.description_source_url(f"GitHub: {url}#issuecomment-1"))
+ def test_legacy_source_sections_accept_bom_and_paragraph_whitespace(self):
+ issue_url = MODULE.source_url("issue", 394, REPOSITORY)
+ pr_url = MODULE.source_url("pr", 395, REPOSITORY)
+ cases = (
+ (f"来源:GitHub Issue #394 \ufeff{issue_url}\ufeff \ufeff 问题现象", issue_url),
+ (f"来源:GitHub PR #395\n\n{pr_url}\n\n问题\nDetails", pr_url),
+ (f'
来源:GitHub PR #395
Details
', pr_url),
+ )
+ for description, expected in cases:
+ with self.subTest(description=description):
+ self.assertEqual(MODULE.description_source_url(description), expected.casefold())
+
+ def test_legacy_source_sections_reject_quoted_or_mismatched_sources(self):
+ for description in (
+ "Body first\n来源:GitHub Issue #394\nhttps://github.com/MemTensor/memmy-agent/issues/394",
+ "来源:GitHub Issue #394\nhttps://github.com/MemTensor/memmy-agent/issues/395",
+ "来源:GitHub PR #395\nhttps://github.com/MemTensor/memmy-agent/issues/395",
+ "来源:GitHub Issue #394\nhttps://example.com/MemTensor/memmy-agent/issues/394",
+ "来源:GitHub Issue #394\nhttps://github.com/MemTensor/memmy-agent/issues/394#issuecomment-1",
+ "来源:GitHub Issue #394\nUnrelated text\nhttps://github.com/MemTensor/memmy-agent/issues/394",
+ ):
+ with self.subTest(description=description):
+ self.assertIsNone(MODULE.description_source_url(description))
+
+ def test_title_cleanup_accepts_verified_legacy_source_sections(self):
+ for kind, number in (("issue", 394), ("pr", 395)):
+ with self.subTest(kind=kind):
+ card = source_card(number=number, kind=kind, form="long")
+ card["description"] = (
+ f"来源:GitHub {MODULE.SOURCE_LABELS[kind]} #{number}\n\n"
+ f"{MODULE.source_url(kind, number, REPOSITORY)}\n\nDetails"
+ )
+ transport = MemoryTransport([card])
+ result, stats = self.rename(transport, apply=True)
+ self.assertEqual((result, stats["renamed"], stats["failed"]), (0, 1, 0))
+ self.assertEqual(transport.cards["card"]["description"], card["description"])
+ writes = [body for method, _, body in transport.calls if method == "PUT"]
+ self.assertEqual(writes, [{"subject": f"[{MODULE.SOURCE_LABELS[kind]} #{number}] Fix scheduler"}])
+
def test_sync_after_create_and_after_rename_reuses_the_same_card(self):
transport = MemoryTransport()
self.assertEqual(self.sync(transport), "created")
diff --git a/scripts/yunxiao_github_sync.py b/scripts/yunxiao_github_sync.py
index 4f8a00314..8bee1a1d5 100644
--- a/scripts/yunxiao_github_sync.py
+++ b/scripts/yunxiao_github_sync.py
@@ -170,7 +170,20 @@ def description_source_url(description: Any) -> str | None:
r"GitHub:\s+(https://github\.com/[^/\s]+/[^/\s]+/(?:issues|pull)/[1-9][0-9]*/?)",
first_line,
)
- return match.group(1).rstrip("/").casefold() if match else None
+ if match:
+ return match.group(1).rstrip("/").casefold()
+ # Early manually imported cards used a source section before the URL.
+ legacy = re.match(
+ r"\A来源[::]\s*GitHub\s+(Issue|PR)\s+#([1-9][0-9]*)\s+"
+ r"(https://github\.com/[^/\s]+/[^/\s]+/(issues|pull)/([1-9][0-9]*)/?)(?=\s|$)",
+ "".join(parser.parts).replace("\ufeff", " ").lstrip(),
+ )
+ if not legacy:
+ return None
+ label, number, url, path, url_number = legacy.groups()
+ if number != url_number or {"Issue": "issues", "PR": "pull"}[label] != path:
+ return None
+ return url.rstrip("/").casefold()
def iso_after_days(value: str, days: int) -> str: