Files
larksuite-cli/internal/skillscheck/sync_test.go
zhangheng023 3a3fc31d0b feat: add incremental skills sync (#965)
* feat: add incremental skills sync

* fix: address skills sync review feedback
2026-05-20 16:27:07 +08:00

223 lines
7.2 KiB
Go

// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package skillscheck
import (
"fmt"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
"time"
"github.com/larksuite/cli/internal/selfupdate"
)
func TestParseSkillsList(t *testing.T) {
input := `Installed skills:
- lark-calendar
- lark-mail
lark-im
custom-skill
lark-base@1.0.0
lark-cli-harness:dev@0.1.0
`
got := ParseSkillsList(input)
want := []string{"custom-skill", "lark-base", "lark-calendar", "lark-cli-harness:dev", "lark-im", "lark-mail"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("ParseSkillsList() = %#v, want %#v", got, want)
}
}
func TestPlanNormal_WithReadableStatePreservesDeletedAndAddsNew(t *testing.T) {
previous := &SkillsState{OfficialSkills: []string{"lark-calendar", "lark-mail"}}
got := PlanSync(SyncInput{
Version: "1.0.33",
OfficialSkills: []string{"lark-calendar", "lark-mail", "lark-new"},
LocalSkills: []string{"lark-calendar", "lark-custom"},
PreviousState: previous,
StateReadable: true,
Force: false,
})
assertStrings(t, got.ToUpdate, []string{"lark-calendar", "lark-new"})
assertStrings(t, got.Added, []string{"lark-new"})
assertStrings(t, got.SkippedDeleted, []string{"lark-mail"})
}
func TestPlanNormal_MissingStateInstallsAllOfficial(t *testing.T) {
got := PlanSync(SyncInput{
Version: "1.0.33",
OfficialSkills: []string{"lark-calendar", "lark-mail", "lark-new"},
LocalSkills: []string{"lark-calendar"},
StateReadable: false,
Force: false,
})
assertStrings(t, got.ToUpdate, []string{"lark-calendar", "lark-mail", "lark-new"})
assertStrings(t, got.Added, []string{"lark-calendar", "lark-mail", "lark-new"})
assertStrings(t, got.SkippedDeleted, []string{})
}
func TestPlanForceRestoresAllOfficial(t *testing.T) {
got := PlanSync(SyncInput{
Version: "1.0.33",
OfficialSkills: []string{"lark-calendar", "lark-mail", "lark-new"},
LocalSkills: []string{"lark-calendar"},
PreviousState: &SkillsState{OfficialSkills: []string{"lark-calendar", "lark-mail"}},
StateReadable: true,
Force: true,
})
assertStrings(t, got.ToUpdate, []string{"lark-calendar", "lark-mail", "lark-new"})
assertStrings(t, got.Added, []string{})
assertStrings(t, got.SkippedDeleted, []string{})
}
type fakeSkillsRunner struct {
officialOut string
globalOut string
officialErr error
globalErr error
installErr map[string]error
installed []string
}
func (f *fakeSkillsRunner) ListOfficialSkills() *selfupdate.NpmResult {
r := &selfupdate.NpmResult{}
r.Stdout.WriteString(f.officialOut)
r.Err = f.officialErr
return r
}
func (f *fakeSkillsRunner) ListGlobalSkills() *selfupdate.NpmResult {
r := &selfupdate.NpmResult{}
r.Stdout.WriteString(f.globalOut)
r.Err = f.globalErr
return r
}
func (f *fakeSkillsRunner) InstallSkill(name string) *selfupdate.NpmResult {
f.installed = append(f.installed, name)
r := &selfupdate.NpmResult{}
if f.installErr != nil {
r.Err = f.installErr[name]
}
return r
}
func TestSyncSkills_WritesStateAndDoesNotWriteStamp(t *testing.T) {
dir := t.TempDir()
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", dir)
if err := WriteState(SkillsState{
Version: "1.0.30",
OfficialSkills: []string{"lark-calendar", "lark-mail"},
UpdatedAt: "2026-05-18T00:00:00Z",
}); err != nil {
t.Fatal(err)
}
runner := &fakeSkillsRunner{
officialOut: "lark-calendar\nlark-mail\nlark-new\n",
globalOut: "lark-calendar\nlark-custom\n",
}
result := SyncSkills(SyncOptions{
Version: "1.0.33",
Runner: runner,
Now: func() time.Time { return time.Date(2026, 5, 18, 12, 0, 0, 0, time.UTC) },
})
if result.Err != nil {
t.Fatalf("SyncSkills() err = %v, want nil", result.Err)
}
assertStrings(t, runner.installed, []string{"lark-calendar", "lark-new"})
state, readable, err := ReadState()
if err != nil || !readable {
t.Fatalf("ReadState() = (_, %v, %v), want readable", readable, err)
}
assertStrings(t, state.OfficialSkills, []string{"lark-calendar", "lark-mail", "lark-new"})
assertStrings(t, state.UpdatedSkills, []string{"lark-calendar", "lark-new"})
assertStrings(t, state.AddedSkills, []string{"lark-new"})
assertStrings(t, state.SkippedDeletedSkills, []string{"lark-mail"})
if _, err := os.Stat(filepath.Join(dir, "skills.stamp")); !os.IsNotExist(err) {
t.Fatalf("skills.stamp exists or stat failed with unexpected err: %v", err)
}
}
func TestSyncSkills_ListFailureDoesNotInstallOrWriteState(t *testing.T) {
dir := t.TempDir()
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", dir)
runner := &fakeSkillsRunner{officialErr: fmt.Errorf("list failed")}
result := SyncSkills(SyncOptions{Version: "1.0.33", Runner: runner, Now: time.Now})
if result.Err == nil || !strings.Contains(result.Err.Error(), "failed to list official skills") {
t.Fatalf("SyncSkills() err = %v, want official list failure", result.Err)
}
if len(runner.installed) != 0 {
t.Fatalf("installed = %#v, want none", runner.installed)
}
if _, readable, err := ReadState(); err != nil || readable {
t.Fatalf("ReadState() = (_, %v, %v), want unreadable missing state", readable, err)
}
}
func TestSyncSkills_GlobalListFailureDoesNotInstallOrWriteState(t *testing.T) {
dir := t.TempDir()
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", dir)
runner := &fakeSkillsRunner{
officialOut: "lark-calendar\nlark-mail\n",
globalErr: fmt.Errorf("global list failed"),
}
result := SyncSkills(SyncOptions{Version: "1.0.33", Runner: runner, Now: time.Now})
if result.Err == nil || !strings.Contains(result.Err.Error(), "failed to list installed skills") {
t.Fatalf("SyncSkills() err = %v, want installed list failure", result.Err)
}
if len(runner.installed) != 0 {
t.Fatalf("installed = %#v, want none", runner.installed)
}
if _, readable, err := ReadState(); err != nil || readable {
t.Fatalf("ReadState() = (_, %v, %v), want unreadable missing state", readable, err)
}
}
func TestSyncSkills_InstallFailureContinuesAndDoesNotWriteState(t *testing.T) {
dir := t.TempDir()
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", dir)
runner := &fakeSkillsRunner{
officialOut: "lark-calendar\nlark-mail\n",
globalOut: "lark-calendar\nlark-mail\n",
installErr: map[string]error{"lark-calendar": fmt.Errorf("boom")},
}
result := SyncSkills(SyncOptions{Version: "1.0.33", Runner: runner, Now: time.Now})
if result.Err == nil || !strings.Contains(result.Err.Error(), "1 skill(s) failed") {
t.Fatalf("SyncSkills() err = %v, want install failure", result.Err)
}
assertStrings(t, runner.installed, []string{"lark-calendar", "lark-mail"})
assertStrings(t, result.Failed, []string{"lark-calendar"})
if !strings.Contains(result.Detail, "boom") {
t.Fatalf("SyncSkills() detail = %q, want install error text", result.Detail)
}
if _, readable, err := ReadState(); err != nil || readable {
t.Fatalf("ReadState() = (_, %v, %v), want no success state", readable, err)
}
}
func TestSyncSkills_NilRunnerFails(t *testing.T) {
result := SyncSkills(SyncOptions{Version: "1.0.33", Now: time.Now})
if result.Err == nil || !strings.Contains(result.Err.Error(), "skills runner is nil") {
t.Fatalf("SyncSkills() err = %v, want nil runner failure", result.Err)
}
}
func assertStrings(t *testing.T, got, want []string) {
t.Helper()
if !reflect.DeepEqual(got, want) {
t.Fatalf("got %#v, want %#v", got, want)
}
}