431 lines
11 KiB
Go
431 lines
11 KiB
Go
// Copyright 2026 Alibaba Group
|
|
// Licensed under the Apache License, Version 2.0
|
|
|
|
package app
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
// --- ensureV ---
|
|
|
|
func TestEnsureV(t *testing.T) {
|
|
tests := []struct {
|
|
in string
|
|
want string
|
|
}{
|
|
{"1.0.6", "v1.0.6"},
|
|
{"v1.0.6", "v1.0.6"},
|
|
{"0.0.1", "v0.0.1"},
|
|
{"dev", "dev"},
|
|
{"unknown", "unknown"},
|
|
{"", "v0.0.0"},
|
|
{"v", "v"},
|
|
}
|
|
for _, tt := range tests {
|
|
got := ensureV(tt.in)
|
|
if got != tt.want {
|
|
t.Errorf("ensureV(%q) = %q, want %q", tt.in, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- parseChangelogEntries ---
|
|
|
|
func TestParseChangelogEntries(t *testing.T) {
|
|
body := `## Changelog
|
|
* abcdef1234567 - fix login bug
|
|
* 0123456789abc Merge branch 'main' into main
|
|
* fedcba9876543 - add upgrade command
|
|
* deadbeef12345 Merge pull request #42
|
|
* 1234567890abc - improve error handling
|
|
`
|
|
entries := parseChangelogEntries(body, 10)
|
|
|
|
if len(entries) != 3 {
|
|
t.Fatalf("len(entries) = %d, want 3 (merge commits should be filtered)", len(entries))
|
|
}
|
|
if entries[0] != "fix login bug" {
|
|
t.Errorf("entries[0] = %q, want %q", entries[0], "fix login bug")
|
|
}
|
|
if entries[1] != "add upgrade command" {
|
|
t.Errorf("entries[1] = %q, want %q", entries[1], "add upgrade command")
|
|
}
|
|
if entries[2] != "improve error handling" {
|
|
t.Errorf("entries[2] = %q, want %q", entries[2], "improve error handling")
|
|
}
|
|
}
|
|
|
|
func TestParseChangelogEntries_MaxLimit(t *testing.T) {
|
|
body := "* abc1234 - msg1\n* def5678 - msg2\n* ghi9012 - msg3\n"
|
|
entries := parseChangelogEntries(body, 2)
|
|
if len(entries) != 2 {
|
|
t.Errorf("len = %d, want 2 (should respect maxEntries)", len(entries))
|
|
}
|
|
}
|
|
|
|
func TestParseChangelogEntries_EmptyBody(t *testing.T) {
|
|
entries := parseChangelogEntries("", 10)
|
|
if len(entries) != 0 {
|
|
t.Errorf("len = %d, want 0 for empty body", len(entries))
|
|
}
|
|
}
|
|
|
|
func TestParseChangelogEntries_OnlyHeaders(t *testing.T) {
|
|
body := "## Changelog\n## Another heading\n"
|
|
entries := parseChangelogEntries(body, 10)
|
|
if len(entries) != 0 {
|
|
t.Errorf("len = %d, want 0 for headers-only body", len(entries))
|
|
}
|
|
}
|
|
|
|
func TestParseChangelogEntries_OnlyMergeCommits(t *testing.T) {
|
|
body := "* abc1234 Merge branch 'main'\n* def5678 Merge pull request #10\n"
|
|
entries := parseChangelogEntries(body, 10)
|
|
if len(entries) != 0 {
|
|
t.Errorf("len = %d, want 0 (all merge commits should be filtered)", len(entries))
|
|
}
|
|
}
|
|
|
|
func TestParseChangelogEntries_DashPrefixedLines(t *testing.T) {
|
|
body := "- fix bug\n- add feature\n"
|
|
entries := parseChangelogEntries(body, 10)
|
|
if len(entries) != 2 {
|
|
t.Fatalf("len = %d, want 2", len(entries))
|
|
}
|
|
if entries[0] != "fix bug" {
|
|
t.Errorf("entries[0] = %q, want %q", entries[0], "fix bug")
|
|
}
|
|
}
|
|
|
|
// --- stripCommitHash ---
|
|
|
|
func TestStripCommitHash(t *testing.T) {
|
|
tests := []struct {
|
|
in string
|
|
want string
|
|
}{
|
|
{"abcdef1234567 - fix bug", "fix bug"},
|
|
{"abcdef1234567 fix bug", "fix bug"},
|
|
{"short", "short"}, // too short to be a hash
|
|
{"abc123", "abc123"}, // less than 7 hex chars
|
|
{"no hash here", "no hash here"},
|
|
{"ABCDEF1234567 - upper case hash", "upper case hash"},
|
|
}
|
|
for _, tt := range tests {
|
|
got := stripCommitHash(tt.in)
|
|
if got != tt.want {
|
|
t.Errorf("stripCommitHash(%q) = %q, want %q", tt.in, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- isNoiseCommit ---
|
|
|
|
func TestIsNoiseCommit(t *testing.T) {
|
|
tests := []struct {
|
|
msg string
|
|
want bool
|
|
}{
|
|
{"Merge branch 'main'", true},
|
|
{"merge branch 'develop'", true},
|
|
{"Merge pull request #42", true},
|
|
{"Merge remote-tracking branch 'origin/main'", true},
|
|
{"fix login bug", false},
|
|
{"add new feature", false},
|
|
{"merge conflicts resolved", false}, // doesn't start with "merge branch"
|
|
}
|
|
for _, tt := range tests {
|
|
got := isNoiseCommit(tt.msg)
|
|
if got != tt.want {
|
|
t.Errorf("isNoiseCommit(%q) = %v, want %v", tt.msg, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- truncateChangelog ---
|
|
|
|
func TestTruncateChangelog(t *testing.T) {
|
|
body := "## Changelog\n* abc1234 - fix A\n* def5678 - fix B\n* ghi9012 - fix C\n* jkl3456 - fix D\n"
|
|
result := truncateChangelog(body)
|
|
if result == "" {
|
|
t.Error("truncateChangelog returned empty")
|
|
}
|
|
// Should contain max 3 entries separated by "; "
|
|
parts := strings.Split(result, "; ")
|
|
if len(parts) > 3 {
|
|
t.Errorf("truncateChangelog should have at most 3 entries, got %d", len(parts))
|
|
}
|
|
}
|
|
|
|
func TestTruncateChangelog_EmptyBody(t *testing.T) {
|
|
if got := truncateChangelog(""); got != "" {
|
|
t.Errorf("truncateChangelog('') = %q, want empty", got)
|
|
}
|
|
}
|
|
|
|
// --- truncateChangelogForList ---
|
|
|
|
func TestTruncateChangelogForList(t *testing.T) {
|
|
tests := []struct {
|
|
body string
|
|
maxLen int
|
|
want string
|
|
}{
|
|
{"", 40, "-"},
|
|
{"## Changelog\n", 40, "-"},
|
|
}
|
|
for _, tt := range tests {
|
|
got := truncateChangelogForList(tt.body, tt.maxLen)
|
|
if got != tt.want {
|
|
t.Errorf("truncateChangelogForList(%q, %d) = %q, want %q", tt.body, tt.maxLen, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTruncateChangelogForList_Truncation(t *testing.T) {
|
|
body := "* abc1234 - a very long commit message that should be truncated eventually\n"
|
|
result := truncateChangelogForList(body, 20)
|
|
if len(result) > 20 {
|
|
t.Errorf("result len = %d, want <= 20", len(result))
|
|
}
|
|
if !strings.HasSuffix(result, "...") {
|
|
t.Errorf("truncated result should end with '...' , got %q", result)
|
|
}
|
|
}
|
|
|
|
// --- progressBar ---
|
|
|
|
func TestProgressBar(t *testing.T) {
|
|
tests := []struct {
|
|
percent float64
|
|
filled int
|
|
}{
|
|
{0, 0},
|
|
{50, 10},
|
|
{100, 20},
|
|
{150, 20}, // capped
|
|
}
|
|
for _, tt := range tests {
|
|
bar := progressBar(tt.percent)
|
|
if len(bar) != 20*len("█") && len(bar) != 20*len("░") {
|
|
// Since multi-byte chars, just check total rune count
|
|
runes := []rune(bar)
|
|
if len(runes) != 20 {
|
|
t.Errorf("progressBar(%v) rune count = %d, want 20", tt.percent, len(runes))
|
|
}
|
|
}
|
|
filledCount := strings.Count(bar, "█")
|
|
if filledCount != tt.filled {
|
|
t.Errorf("progressBar(%v) filled = %d, want %d", tt.percent, filledCount, tt.filled)
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- shortenHome ---
|
|
|
|
func TestShortenHome(t *testing.T) {
|
|
// Non-home path should be unchanged
|
|
got := shortenHome("/tmp/somewhere")
|
|
if got != "/tmp/somewhere" {
|
|
t.Errorf("shortenHome(/tmp/somewhere) = %q", got)
|
|
}
|
|
}
|
|
|
|
// --- resolveUpgradeFormat ---
|
|
|
|
func TestResolveUpgradeFormat_Default(t *testing.T) {
|
|
root := &cobra.Command{}
|
|
root.PersistentFlags().String("format", "json", "output format")
|
|
child := &cobra.Command{}
|
|
root.AddCommand(child)
|
|
|
|
// format not changed => should default to "table" for upgrade
|
|
got := resolveUpgradeFormat(child)
|
|
if got != "table" {
|
|
t.Errorf("resolveUpgradeFormat(unchanged) = %q, want %q", got, "table")
|
|
}
|
|
}
|
|
|
|
func TestResolveUpgradeFormat_ExplicitJSON(t *testing.T) {
|
|
root := &cobra.Command{}
|
|
root.PersistentFlags().String("format", "json", "output format")
|
|
child := &cobra.Command{}
|
|
root.AddCommand(child)
|
|
|
|
// Simulate user explicitly setting format
|
|
root.PersistentFlags().Set("format", "json")
|
|
|
|
got := resolveUpgradeFormat(child)
|
|
if got != "json" {
|
|
t.Errorf("resolveUpgradeFormat(explicit json) = %q, want %q", got, "json")
|
|
}
|
|
}
|
|
|
|
func TestResolveUpgradeFormat_ExplicitTable(t *testing.T) {
|
|
root := &cobra.Command{}
|
|
root.PersistentFlags().String("format", "json", "output format")
|
|
child := &cobra.Command{}
|
|
root.AddCommand(child)
|
|
|
|
root.PersistentFlags().Set("format", "table")
|
|
|
|
got := resolveUpgradeFormat(child)
|
|
if got != "table" {
|
|
t.Errorf("resolveUpgradeFormat(explicit table) = %q, want %q", got, "table")
|
|
}
|
|
}
|
|
|
|
// --- writeJSON ---
|
|
|
|
func TestWriteJSON(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
data := map[string]any{
|
|
"version": "v1.0.6",
|
|
"ok": true,
|
|
}
|
|
if err := writeJSON(&buf, data); err != nil {
|
|
t.Fatalf("writeJSON() error = %v", err)
|
|
}
|
|
output := buf.String()
|
|
if !strings.Contains(output, `"version": "v1.0.6"`) {
|
|
t.Errorf("output missing version: %s", output)
|
|
}
|
|
if !strings.Contains(output, `"ok": true`) {
|
|
t.Errorf("output missing ok: %s", output)
|
|
}
|
|
}
|
|
|
|
// --- strictVerifyFile ---
|
|
|
|
func TestStrictVerifyFile_MatchesChecksums(t *testing.T) {
|
|
dir := t.TempDir()
|
|
filePath := filepath.Join(dir, "test.tar.gz")
|
|
content := []byte("valid binary content")
|
|
os.WriteFile(filePath, content, 0644)
|
|
|
|
hash := computeTestSHA256(t, content)
|
|
checksums := hash + " test.tar.gz\n"
|
|
|
|
err := strictVerifyFile("[1/5]", filePath, "test.tar.gz", "", checksums)
|
|
if err != nil {
|
|
t.Errorf("expected success, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestStrictVerifyFile_ChecksumMismatch(t *testing.T) {
|
|
dir := t.TempDir()
|
|
filePath := filepath.Join(dir, "test.tar.gz")
|
|
os.WriteFile(filePath, []byte("tampered content"), 0644)
|
|
|
|
checksums := "0000000000000000000000000000000000000000000000000000000000000000 test.tar.gz\n"
|
|
|
|
err := strictVerifyFile("[1/5]", filePath, "test.tar.gz", "", checksums)
|
|
if err == nil {
|
|
t.Fatal("expected error for checksum mismatch")
|
|
}
|
|
if !strings.Contains(err.Error(), "校验失败") {
|
|
t.Errorf("error = %q, want to contain '校验失败'", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestStrictVerifyFile_DigestMismatch(t *testing.T) {
|
|
dir := t.TempDir()
|
|
filePath := filepath.Join(dir, "test.tar.gz")
|
|
os.WriteFile(filePath, []byte("tampered"), 0644)
|
|
|
|
err := strictVerifyFile("[1/5]", filePath, "test.tar.gz",
|
|
"sha256:0000000000000000000000000000000000000000000000000000000000000000",
|
|
"")
|
|
if err == nil {
|
|
t.Fatal("expected error for digest mismatch")
|
|
}
|
|
}
|
|
|
|
func TestStrictVerifyFile_NoChecksumInfo(t *testing.T) {
|
|
dir := t.TempDir()
|
|
filePath := filepath.Join(dir, "test.tar.gz")
|
|
os.WriteFile(filePath, []byte("content"), 0644)
|
|
|
|
err := strictVerifyFile("[1/5]", filePath, "test.tar.gz", "", "")
|
|
if err != nil {
|
|
t.Errorf("no checksum info should skip, not error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestStrictVerifyFile_FileNotInChecksums_FallsToDigest(t *testing.T) {
|
|
dir := t.TempDir()
|
|
filePath := filepath.Join(dir, "skills.zip")
|
|
content := []byte("skills content")
|
|
os.WriteFile(filePath, content, 0644)
|
|
|
|
hash := computeTestSHA256(t, content)
|
|
// checksums.txt has entries but NOT skills.zip
|
|
checksums := "abcdef1234567890 other-file.tar.gz\n"
|
|
|
|
err := strictVerifyFile("[1/5]", filePath, "skills.zip", "sha256:"+hash, checksums)
|
|
if err != nil {
|
|
t.Errorf("should fall through to digest and succeed: %v", err)
|
|
}
|
|
}
|
|
|
|
func computeTestSHA256(t *testing.T, data []byte) string {
|
|
t.Helper()
|
|
h := sha256.Sum256(data)
|
|
return hex.EncodeToString(h[:])
|
|
}
|
|
|
|
// --- newUpgradeCommand ---
|
|
|
|
func TestNewUpgradeCommand_Flags(t *testing.T) {
|
|
cmd := newUpgradeCommand()
|
|
|
|
if cmd.Use != "upgrade" {
|
|
t.Errorf("Use = %q, want upgrade", cmd.Use)
|
|
}
|
|
|
|
expectedFlags := []string{"check", "list", "version", "rollback", "force", "skip-skills"}
|
|
for _, name := range expectedFlags {
|
|
if cmd.Flags().Lookup(name) == nil {
|
|
t.Errorf("missing flag: --%s", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNewUpgradeCommand_NoArgs(t *testing.T) {
|
|
cmd := newUpgradeCommand()
|
|
// Simulate passing positional args - should error with cobra.NoArgs
|
|
cmd.SetArgs([]string{"rollback"})
|
|
err := cmd.Execute()
|
|
if err == nil {
|
|
t.Error("expected error for positional args (NoArgs)")
|
|
}
|
|
}
|
|
|
|
func TestNewUpgradeCommand_Help(t *testing.T) {
|
|
cmd := newUpgradeCommand()
|
|
var buf bytes.Buffer
|
|
cmd.SetOut(&buf)
|
|
cmd.SetArgs([]string{"--help"})
|
|
cmd.Execute()
|
|
help := buf.String()
|
|
|
|
if !strings.Contains(help, "upgrade") {
|
|
t.Error("help should contain 'upgrade'")
|
|
}
|
|
if !strings.Contains(help, "--check") {
|
|
t.Error("help should contain --check")
|
|
}
|
|
if !strings.Contains(help, "--rollback") {
|
|
t.Error("help should contain --rollback")
|
|
}
|
|
}
|