Files
knowledge-governance/internal/versionhistory/rules_test.go
T
姚章浩 4cb5110845
build / build (push) Successful in 1m1s
fix(version-history): 页面链接改走公网 origin(抓取仍走内网)+ tag 补链接
现象(主人反馈):点「查看提交」打开是空白页。原因:链接用的是**抓取用的内网地址**
(`http://192.168.50.223:3000/...`),内网外的浏览器到不了;而公网地址
`https://gitea.zheromiku.top/...` 实测 200。另外 tag 证据里**根本没有 url**,
页面渲染出空 `href`,点下去像"没反应"。

修复:

- 新增 `VERSION_HISTORY_LINK_ORIGIN`(页面链接 origin,公网;留空 = 与抓取 origin 相同):
  抓取仍匿名走内网白名单地址,**给人点的链接**用公网 origin 拼 —— 两者本就不该共用一个地址;
  校验必须是干净的 http(s) 绝对地址。
- tag 证据补 `url`(`<link origin>/<repo>/src/tag/<name>`),并在前端**没有链接就不渲染**该按钮。
- 测试:客户端(LinkOrigin 生效 / 未配时退回 / 非法拒绝)、规则(tag 链接写入证据 / 无基址不编链接)、
  前端(有链接才渲染「查看 tag」)。

验证:make verify 15/15;真实链路重同步后库里 commit_url = `https://gitea.zheromiku.top/...`(200)、
release_tags 带 url。
2026-10-14 11:06:13 +08:00

263 lines
12 KiB
Go
Raw 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.
package versionhistory
import (
"testing"
"time"
"knowledge-governance/internal/governance"
)
func at(minute int) time.Time {
return time.Date(2026, 10, 13, 10, minute, 0, 0, time.UTC)
}
func commit(sha string, parents []string, minute int) RawCommit {
return RawCommit{SHA: sha, Parents: parents, CommittedAt: at(minute),
AuthorDisplay: "姚章浩", Subject: "feat: " + sha, Body: "body " + sha}
}
func changelogWith(top string) string {
if top == "" {
return "# Changelog\n\n## [Unreleased]\n\n### Added\n\n- 早期提交,还没有已发布版本\n"
}
return "# Changelog\n\n## [Unreleased]\n\n### Added\n\n- 新\n\n" +
"## [" + top + "] - 2026-10-01\n\n### Added\n\n- 首次发布\n"
}
func readerFor(topBySHA map[string]string) ChangelogSource {
return func(sha string) (string, error) { return changelogWith(topBySHA[sha]), nil }
}
func find(t *testing.T, records []governance.VersionHistoryCommit, sha string) governance.VersionHistoryCommit {
t.Helper()
for _, record := range records {
if record.CommitSHA == sha {
return record
}
}
t.Fatalf("记录里没有提交 %s", sha)
return governance.VersionHistoryCommit{}
}
func TestDeriveVersionsLinearHistoryWithAnnotatedTag(t *testing.T) {
commits := []RawCommit{
commit("A", nil, 1), commit("B", []string{"A"}, 2),
commit("C", []string{"B"}, 3), commit("D", []string{"C"}, 4),
}
tags := []RawTag{{Name: "v0.1.0", TagSHA: "tagobj", TargetSHA: "A", Annotated: true}}
top := map[string]string{"A": "0.1.0", "B": "0.1.0", "C": "0.1.0", "D": "0.1.0"}
result := DeriveVersions(DeriveInput{RepoKey: "Weknora/knowledge-governance",
CommitURLBase: "http://gitea/commit/", Commits: commits, Tags: tags, Changelog: readerFor(top)})
if !result.GraphComplete {
t.Fatal("线性历史应判为图完整")
}
if len(result.Records) != 4 {
t.Fatalf("记录数 = %d,期望 4", len(result.Records))
}
// 最新在上
if result.Records[0].CommitSHA != "D" || result.Records[3].CommitSHA != "A" {
t.Fatalf("排序不对:%s…%s", result.Records[0].CommitSHA, result.Records[3].CommitSHA)
}
for sha, want := range map[string]string{"A": "0.1.0", "B": "0.1.1", "C": "0.1.2", "D": "0.1.3"} {
record := find(t, result.Records, sha)
if record.VersionLabel != want {
t.Fatalf("%s 的号 = %q,期望 %q(原因 %s)", sha, record.VersionLabel, want, record.VersionEvidence.Reason)
}
if record.CommitURL != "http://gitea/commit/"+sha {
t.Fatalf("%s 的提交链接 = %q", sha, record.CommitURL)
}
}
release := find(t, result.Records, "A")
if release.RecordKind != governance.RecordKindRelease || release.VersionState != governance.VersionStateVerified {
t.Fatalf("tag 指向的提交应是正式版本:%+v", release)
}
if release.ReleaseNotes == "" || len(release.ReleaseTags) != 1 {
t.Fatalf("正式版本必须带 tag 与提交内 CHANGELOG 正文:%+v", release)
}
development := find(t, result.Records, "B")
if development.RecordKind != governance.RecordKindDevelopment || development.ReleaseNotes != "" {
t.Fatalf("开发提交不该带发布正文:%+v", development)
}
}
func TestDeriveVersionsKeepsMergeAndSideBranchCommitsAndSameNumberDifferentSHA(t *testing.T) {
commits := []RawCommit{
commit("A", nil, 1), commit("B", []string{"A"}, 2), commit("C", []string{"A"}, 3),
commit("M", []string{"B", "C"}, 4), commit("D", []string{"M"}, 5),
}
tags := []RawTag{{Name: "v0.1.0", TargetSHA: "A", Annotated: true}}
top := map[string]string{"A": "0.1.0", "B": "0.1.0", "C": "0.1.0", "M": "0.1.0", "D": "0.1.0"}
result := DeriveVersions(DeriveInput{RepoKey: "repo", Commits: commits, Tags: tags, Changelog: readerFor(top)})
for _, sha := range []string{"B", "C", "M", "D"} {
if record := find(t, result.Records, sha); record.VersionLabel == "" {
t.Fatalf("合并/侧分支提交 %s 必须能查到:%+v", sha, record)
}
}
if merge := find(t, result.Records, "M"); len(merge.Parents) != 2 || merge.VersionLabel != "0.1.3" {
t.Fatalf("合并提交应保留双父并按图算号:%+v", merge)
}
if side := find(t, result.Records, "C"); side.VersionLabel != "0.1.1" {
t.Fatalf("侧分支提交按同一基线算:%+v", side)
}
// 同号不同 SHA:B 与 C 都是 0.1.1,两条都要在(唯一身份是 SHA)
same := 0
for _, record := range result.Records {
if record.VersionLabel == "0.1.1" {
same++
}
}
if same != 2 {
t.Fatalf("同号不同 SHA 应保留两条,实际 %d", same)
}
if find(t, result.Records, "B").CommitSHA == find(t, result.Records, "C").CommitSHA {
t.Fatal("同号两条必须靠 SHA 区分")
}
}
func TestDeriveVersionsIncompleteGraphNeverInventsNumbers(t *testing.T) {
// 只拿到 B、C,父提交 A 不在集合里(等价浅克隆/检出截断)
commits := []RawCommit{commit("B", []string{"A"}, 2), commit("C", []string{"B"}, 3)}
tags := []RawTag{{Name: "v0.1.0", TargetSHA: "A", Annotated: true}}
top := map[string]string{"B": "0.1.0", "C": "0.1.0"}
result := DeriveVersions(DeriveInput{RepoKey: "repo", Commits: commits, Tags: tags, Changelog: readerFor(top)})
if result.GraphComplete {
t.Fatal("缺父提交必须判为图不完整")
}
if len(result.MissingParents) != 1 || result.MissingParents[0] != "A" {
t.Fatalf("缺失父提交 = %v", result.MissingParents)
}
for _, record := range result.Records {
if record.VersionLabel != "" {
t.Fatalf("图不完整时不许给号:%+v", record)
}
if record.VersionEvidence.Reason != governance.ReasonIncompleteGraph {
t.Fatalf("原因码应为 %s,得到 %s", governance.ReasonIncompleteGraph, record.VersionEvidence.Reason)
}
if record.RecordKind != governance.RecordKindHistorical {
t.Fatalf("应为历史提交:%+v", record)
}
}
}
func TestDeriveVersionsLightweightTagIsNotARelease(t *testing.T) {
commits := []RawCommit{commit("A", nil, 1)}
tags := []RawTag{{Name: "v0.1.0", TargetSHA: "A", Annotated: false}} // 轻量 tag
result := DeriveVersions(DeriveInput{RepoKey: "repo", Commits: commits, Tags: tags,
Changelog: readerFor(map[string]string{"A": "0.1.0"})})
record := find(t, result.Records, "A")
if record.RecordKind == governance.RecordKindRelease {
t.Fatalf("轻量 tag 不得标成正式版本:%+v", record)
}
if len(record.ReleaseTags) != 1 || record.ReleaseTags[0].Annotated {
t.Fatalf("tag 证据要原样保留:%+v", record.ReleaseTags)
}
}
func TestDeriveVersionsPicksUpTagAddedAfterwards(t *testing.T) {
commits := []RawCommit{commit("A", nil, 1), commit("B", []string{"A"}, 2)}
changelog := readerFor(map[string]string{"A": "0.1.0", "B": "0.1.0"})
before := DeriveVersions(DeriveInput{RepoKey: "repo", Commits: commits, Changelog: changelog})
if find(t, before.Records, "B").RecordKind != governance.RecordKindHistorical {
t.Fatalf("还没有 tag 时 B 只能是历史提交:%+v", find(t, before.Records, "B"))
}
// 旧提交后补的注释 tag:再算一次就该升级成正式版本相关记录(不新增第二行)
after := DeriveVersions(DeriveInput{RepoKey: "repo", Commits: commits, Changelog: changelog,
Tags: []RawTag{{Name: "v0.1.0", TargetSHA: "A", Annotated: true}}})
if find(t, after.Records, "A").RecordKind != governance.RecordKindRelease {
t.Fatalf("后补的注释 tag 必须让 A 变成正式版本:%+v", find(t, after.Records, "A"))
}
if len(after.Records) != len(before.Records) {
t.Fatalf("后补 tag 不得新增记录:%d → %d", len(before.Records), len(after.Records))
}
if find(t, after.Records, "B").VersionLabel != "0.1.1" {
t.Fatalf("B 应拿到开发号:%+v", find(t, after.Records, "B"))
}
}
func TestDeriveVersionsFillsTagLinksForHumans(t *testing.T) {
commits := []RawCommit{commit("A", nil, 1)}
tags := []RawTag{{Name: "v0.1.0", TargetSHA: "A", Annotated: true}}
derived := DeriveVersions(DeriveInput{RepoKey: "repo", Commits: commits, Tags: tags,
CommitURLBase: "https://gitea.example/o/r/commit/",
TagURLBase: "https://gitea.example/o/r/src/tag/",
Changelog: readerFor(map[string]string{"A": "0.1.0"})})
record := find(t, derived.Records, "A")
if len(record.ReleaseTags) != 1 || record.ReleaseTags[0].URL != "https://gitea.example/o/r/src/tag/v0.1.0" {
t.Fatalf("tag 必须带可点的链接:%+v", record.ReleaseTags)
}
// 不给 TagURLBase ⇒ 不下发链接(页面据此不渲染空的 tag 链接)
bare := DeriveVersions(DeriveInput{RepoKey: "repo", Commits: commits, Tags: tags,
Changelog: readerFor(map[string]string{"A": "0.1.0"})})
if url := find(t, bare.Records, "A").ReleaseTags[0].URL; url != "" {
t.Fatalf("没有基址时不该编链接:%q", url)
}
}
func TestDeriveVersionsSortsDeterministicallyForPaging(t *testing.T) {
sameTime := at(9)
commits := []RawCommit{
{SHA: "aaa", CommittedAt: sameTime}, {SHA: "bbb", CommittedAt: sameTime},
{SHA: "ccc", CommittedAt: sameTime}, {SHA: "ddd", CommittedAt: sameTime},
}
result := DeriveVersions(DeriveInput{RepoKey: "repo", Commits: commits})
order := []string{result.Records[0].CommitSHA, result.Records[1].CommitSHA,
result.Records[2].CommitSHA, result.Records[3].CommitSHA}
want := []string{"ddd", "ccc", "bbb", "aaa"}
for i := range want {
if order[i] != want[i] {
t.Fatalf("同一提交时间的排序必须确定:%v", order)
}
}
}
// 回归用例(真实数据发现):tag **更晚**才在链尾打上时,tag 之前那些「提交内 CHANGELOG 顶部
// 已经写着 0.1.0」的提交**不能**被算成 0.1.0 开发版 —— 它们的祖先集合全被那个更晚的 tag 覆盖,
// 按提交数会得到 0。正确结果:历史提交 + 原因 release_tag_created_later。
func TestDeriveVersionsDoesNotNumberCommitsThatPredateTheTag(t *testing.T) {
commits := []RawCommit{
commit("A", nil, 1), commit("B", []string{"A"}, 2),
commit("C", []string{"B"}, 3), commit("D", []string{"C"}, 4),
}
// tag 打在 C 上;A、B 的 CHANGELOG 顶部**也已经**是 0.1.0(旧分支遗留的条目)
tags := []RawTag{{Name: "v0.1.0", TargetSHA: "C", Annotated: true}}
top := map[string]string{"A": "0.1.0", "B": "0.1.0", "C": "0.1.0", "D": "0.1.0"}
result := DeriveVersions(DeriveInput{RepoKey: "repo", Commits: commits, Tags: tags, Changelog: readerFor(top)})
for _, sha := range []string{"A", "B"} {
record := find(t, result.Records, sha)
if record.VersionLabel != "" || record.RecordKind != governance.RecordKindHistorical {
t.Fatalf("tag 之前的提交 %s 不许编号:%+v", sha, record)
}
if record.VersionEvidence.Reason != governance.ReasonTagCreatedLater {
t.Fatalf("提交 %s 的原因码 = %q,期望 %q", sha, record.VersionEvidence.Reason, governance.ReasonTagCreatedLater)
}
}
if release := find(t, result.Records, "C"); release.RecordKind != governance.RecordKindRelease || release.VersionLabel != "0.1.0" {
t.Fatalf("tag 指向的提交才是正式版本:%+v", release)
}
if after := find(t, result.Records, "D"); after.VersionLabel != "0.1.1" {
t.Fatalf("tag 之后的提交才是开发号:%+v", after)
}
}
// 侧分支(不含 tag 的那条线)同样不给号。
func TestDeriveVersionsSideBranchWithoutTagStaysHistorical(t *testing.T) {
commits := []RawCommit{
commit("A", nil, 1), commit("B", []string{"A"}, 2), commit("T", []string{"B"}, 3),
commit("S", []string{"A"}, 4), // 从 A 分出去、不含 tag 的侧分支
commit("M", []string{"T", "S"}, 5), // 合并回主干
}
tags := []RawTag{{Name: "v0.1.0", TargetSHA: "T", Annotated: true}}
top := map[string]string{"A": "0.1.0", "B": "0.1.0", "T": "0.1.0", "S": "0.1.0", "M": "0.1.0"}
result := DeriveVersions(DeriveInput{RepoKey: "repo", Commits: commits, Tags: tags, Changelog: readerFor(top)})
if side := find(t, result.Records, "S"); side.VersionLabel != "" {
t.Fatalf("不含 tag 的侧分支提交不许编号:%+v", side)
}
if merge := find(t, result.Records, "M"); merge.VersionLabel != "0.1.2" {
t.Fatalf("合并提交含 tag 祖先 ⇒ 按图算号:%+v", merge)
}
}