4cb5110845
build / build (push) Successful in 1m1s
现象(主人反馈):点「查看提交」打开是空白页。原因:链接用的是**抓取用的内网地址** (`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。
263 lines
12 KiB
Go
263 lines
12 KiB
Go
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)
|
||
}
|
||
}
|