1
0
Files
nex/versionctl/projectversion/version_test.go
lanyuanxiaoyao bc7a7c6e81 feat: 迁移 versionctl 为独立模块并新增 make version-bump 命令
- 将 backend/cmd/versionctl 和 backend/pkg/projectversion 迁移至独立 versionctl/ Go 模块
- 新增 bump 子命令支持 major/minor/patch 和指定版本号,含版本倒退防护
- 新增 make version-bump 编排完整升迁流程(bump + sync + check + commit + tag)
- 更新所有引用路径:根 Makefile、backend/Makefile、release.yml、.golangci.yml
- 新增 versionctl/.golangci.yml(精简配置)和 Makefile(lint/test/coverage)
- 根 Makefile lint/test 集成 versionctl 模块
- 同步 openspec specs:新增 version-bump spec,更新 release-pipeline spec
2026-05-05 04:18:10 +08:00

216 lines
6.6 KiB
Go

package projectversion
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestParse(t *testing.T) {
t.Run("valid", func(t *testing.T) {
version, err := Parse("1.2.3")
require.NoError(t, err)
assert.Equal(t, Version{Major: 1, Minor: 2, Patch: 3}, version)
assert.Equal(t, "1.2.3", version.String())
})
t.Run("invalid", func(t *testing.T) {
invalidValues := []string{"", "1.2", "1.2.3.4", "v1.2.3", "01.2.3", "1.02.3"}
for _, tc := range invalidValues {
_, err := Parse(tc)
assert.Error(t, err, "%q 应校验失败", tc)
}
})
}
func TestUpdatePackageJSONVersion(t *testing.T) {
content := "{\n \"name\": \"frontend\",\n \"version\": \"0.0.0\"\n}\n"
updated, err := UpdatePackageJSONVersion(content, "1.2.3")
require.NoError(t, err)
assert.Contains(t, updated, `"version": "1.2.3"`)
version, err := ReadPackageJSONVersion(updated)
require.NoError(t, err)
assert.Equal(t, "1.2.3", version)
}
func TestUpsertEnvVar(t *testing.T) {
updated := UpsertEnvVar("VITE_API_BASE=/api\n", "VITE_APP_VERSION", "1.2.3")
assert.Contains(t, updated, "VITE_API_BASE=/api\n")
assert.Contains(t, updated, "VITE_APP_VERSION=1.2.3\n")
updated = UpsertEnvVar(updated, "VITE_APP_VERSION", "2.0.0")
value, ok := ReadEnvVar(updated, "VITE_APP_VERSION")
assert.True(t, ok)
assert.Equal(t, "2.0.0", value)
assert.Equal(t, 1, strings.Count(updated, "VITE_APP_VERSION="))
}
func TestSyncAndCheck(t *testing.T) {
root := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(root, "VERSION"), []byte("1.2.3\n"), 0o600))
require.NoError(t, os.MkdirAll(filepath.Join(root, "frontend"), 0o755))
require.NoError(t, os.WriteFile(filepath.Join(root, "frontend", "package.json"), []byte("{\n \"name\": \"frontend\",\n \"version\": \"0.0.0\"\n}\n"), 0o600))
require.NoError(t, os.WriteFile(filepath.Join(root, "frontend", ".env.production"), []byte("VITE_API_BASE=/api\n"), 0o600))
require.NoError(t, os.WriteFile(filepath.Join(root, "frontend", ".env.development"), []byte("VITE_API_BASE=\n"), 0o600))
require.NoError(t, os.WriteFile(filepath.Join(root, "frontend", ".env.desktop"), []byte("VITE_API_BASE=\n"), 0o600))
require.NoError(t, Sync(root))
require.NoError(t, Check(root))
packageJSONContent, err := os.ReadFile(filepath.Join(root, "frontend", "package.json"))
require.NoError(t, err)
assert.Contains(t, string(packageJSONContent), `"version": "1.2.3"`)
for _, relPath := range frontendVersionFiles {
content, readErr := os.ReadFile(filepath.Join(root, relPath))
require.NoError(t, readErr)
assert.Contains(t, string(content), "VITE_APP_VERSION=1.2.3\n")
}
}
func TestVerifyTag(t *testing.T) {
root := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(root, "VERSION"), []byte("1.2.3\n"), 0o600))
require.NoError(t, VerifyTag(root, "v1.2.3"))
assert.Error(t, VerifyTag(root, "1.2.3"))
assert.Error(t, VerifyTag(root, "v1.2.4"))
}
func TestAssetNames(t *testing.T) {
linuxServer, err := ServerAssetName("1.2.3", "linux", "amd64")
require.NoError(t, err)
assert.Equal(t, "nex-server_1.2.3_linux_amd64.tar.gz", linuxServer)
macServer, err := ServerAssetName("1.2.3", "darwin", "arm64")
require.NoError(t, err)
assert.Equal(t, "nex-server_1.2.3_darwin_arm64.tar.gz", macServer)
macDesktop, err := DesktopAssetName("1.2.3", "macos")
require.NoError(t, err)
assert.Equal(t, "Nex_1.2.3_macOS_universal.zip", macDesktop)
_, err = DesktopAssetName("1.2.3", "ios")
assert.Error(t, err)
}
func TestDesktopInfoPlist(t *testing.T) {
plist, err := DesktopInfoPlist("1.2.3", "13.0")
require.NoError(t, err)
assert.Contains(t, plist, "<key>CFBundleShortVersionString</key>\n <string>1.2.3</string>")
assert.Contains(t, plist, "<key>CFBundleVersion</key>\n <string>1.2.3</string>")
assert.Contains(t, plist, "<key>LSMinimumSystemVersion</key>\n <string>13.0</string>")
_, err = DesktopInfoPlist("1.2", "13.0")
assert.Error(t, err)
_, err = DesktopInfoPlist("1.2.3", "")
assert.Error(t, err)
}
func TestLess(t *testing.T) {
assert.True(t, Version{1, 0, 0}.Less(Version{2, 0, 0}))
assert.True(t, Version{1, 1, 0}.Less(Version{1, 2, 0}))
assert.True(t, Version{1, 0, 1}.Less(Version{1, 0, 2}))
assert.False(t, Version{2, 0, 0}.Less(Version{1, 0, 0}))
assert.False(t, Version{1, 0, 0}.Less(Version{1, 0, 0}))
}
func TestBump(t *testing.T) {
setupRoot := func(t *testing.T) string {
t.Helper()
root := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(root, "VERSION"), []byte("0.1.0\n"), 0o600))
return root
}
t.Run("major", func(t *testing.T) {
root := setupRoot(t)
v, err := Bump(root, "major")
require.NoError(t, err)
assert.Equal(t, Version{1, 0, 0}, v)
read, readErr := ReadString(root)
require.NoError(t, readErr)
assert.Equal(t, "1.0.0", read)
})
t.Run("minor", func(t *testing.T) {
root := setupRoot(t)
v, err := Bump(root, "minor")
require.NoError(t, err)
assert.Equal(t, Version{0, 2, 0}, v)
read, readErr := ReadString(root)
require.NoError(t, readErr)
assert.Equal(t, "0.2.0", read)
})
t.Run("patch", func(t *testing.T) {
root := setupRoot(t)
v, err := Bump(root, "patch")
require.NoError(t, err)
assert.Equal(t, Version{0, 1, 1}, v)
read, readErr := ReadString(root)
require.NoError(t, readErr)
assert.Equal(t, "0.1.1", read)
})
t.Run("specific version", func(t *testing.T) {
root := setupRoot(t)
v, err := Bump(root, "1.0.0")
require.NoError(t, err)
assert.Equal(t, Version{1, 0, 0}, v)
})
t.Run("same version as current", func(t *testing.T) {
root := setupRoot(t)
v, err := Bump(root, "0.1.0")
require.NoError(t, err)
assert.Equal(t, Version{0, 1, 0}, v)
})
t.Run("invalid argument", func(t *testing.T) {
root := setupRoot(t)
_, err := Bump(root, "invalid")
assert.Error(t, err)
})
}
func TestCheckNoRegression(t *testing.T) {
t.Run("greater than existing tag", func(t *testing.T) {
err := CheckNoRegression(Version{0, 2, 0}, []string{"v0.1.0"})
assert.NoError(t, err)
})
t.Run("equal to existing tag", func(t *testing.T) {
err := CheckNoRegression(Version{0, 1, 0}, []string{"v0.1.0"})
assert.Error(t, err)
})
t.Run("less than existing tag", func(t *testing.T) {
err := CheckNoRegression(Version{0, 1, 5}, []string{"v0.2.0"})
assert.Error(t, err)
})
t.Run("no tags", func(t *testing.T) {
err := CheckNoRegression(Version{0, 1, 0}, nil)
assert.NoError(t, err)
})
t.Run("skips non-semver tags", func(t *testing.T) {
err := CheckNoRegression(Version{0, 2, 0}, []string{"v0.1.0", "some-other-tag"})
assert.NoError(t, err)
})
t.Run("picks max tag", func(t *testing.T) {
err := CheckNoRegression(Version{0, 1, 5}, []string{"v0.1.0", "v0.2.0", "v0.0.5"})
assert.Error(t, err)
})
}