bf9395e022
CI / results (push) Blocked by required conditions
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
CI / e2e-live (push) Waiting to run
CI / fast-gate (push) Failing after 0s
Test PR Label Logic / test-pr-labels (push) Failing after 1s
Skill Format Check / check-format (push) Failing after 2s
CI / security (push) Failing after 5s
CI / unit-test (push) Has been skipped
CI / lint (push) Has been skipped
CI / script-test (push) Has been skipped
CI / deterministic-gate (push) Has been skipped
CI / coverage (push) Has been skipped
CI / deadcode (push) Waiting to run
539 lines
18 KiB
Go
539 lines
18 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package selfupdate
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/larksuite/cli/internal/core"
|
|
"github.com/larksuite/cli/internal/vfs"
|
|
)
|
|
|
|
// executableTestFS mocks vfs for tests that still need vfs.Executable.
|
|
type executableTestFS struct {
|
|
vfs.OsFs
|
|
exe string
|
|
}
|
|
|
|
func (f executableTestFS) Executable() (string, error) { return f.exe, nil }
|
|
|
|
// lookPathMock patches execLookPath within VerifyBinary for controlled testing.
|
|
// Do not use t.Parallel() in tests that install this mock — it mutates a package-level var.
|
|
type lookPathMock struct {
|
|
oldLookPath func(string) (string, error)
|
|
result string
|
|
resultErr error
|
|
}
|
|
|
|
func (m *lookPathMock) install(bin string) {
|
|
m.oldLookPath = execLookPath
|
|
execLookPath = func(name string) (string, error) {
|
|
if name == bin {
|
|
return m.result, m.resultErr
|
|
}
|
|
return m.oldLookPath(name)
|
|
}
|
|
}
|
|
|
|
func (m *lookPathMock) restore() {
|
|
execLookPath = m.oldLookPath
|
|
}
|
|
|
|
func TestResolveExe(t *testing.T) {
|
|
u := New()
|
|
p, err := u.resolveExe()
|
|
if err != nil {
|
|
t.Fatalf("resolveExe() error: %v", err)
|
|
}
|
|
if !filepath.IsAbs(p) {
|
|
t.Errorf("expected absolute path, got: %s", p)
|
|
}
|
|
}
|
|
|
|
func TestPrepareSelfReplace_ReturnsNoError(t *testing.T) {
|
|
u := New()
|
|
restore, err := u.PrepareSelfReplace()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
restore()
|
|
}
|
|
|
|
func TestCleanupStaleFiles_NoPanic(t *testing.T) {
|
|
u := New()
|
|
u.CleanupStaleFiles()
|
|
}
|
|
|
|
func TestVerifyBinaryLookPath(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("uses a POSIX shell script")
|
|
}
|
|
|
|
dir := t.TempDir()
|
|
bin := filepath.Join(dir, "lark-cli")
|
|
script := "#!/bin/sh\nif [ \"$1\" = \"--version\" ]; then echo \"lark-cli version 2.1.0\"; exit 0; fi\nexit 12\n"
|
|
if err := os.WriteFile(bin, []byte(script), 0755); err != nil {
|
|
t.Fatalf("write test binary: %v", err)
|
|
}
|
|
|
|
mock := &lookPathMock{result: bin}
|
|
mock.install("lark-cli")
|
|
t.Cleanup(mock.restore)
|
|
|
|
if err := New().VerifyBinary("2.1.0"); err != nil {
|
|
t.Fatalf("VerifyBinary(2.1.0) error = %v, want nil", err)
|
|
}
|
|
|
|
if err := New().VerifyBinary("3.0.0"); err == nil {
|
|
t.Fatal("VerifyBinary(mismatched) expected error, got nil")
|
|
}
|
|
|
|
// Regression: version must match exactly (not substring / prefix).
|
|
if err := New().VerifyBinary("0.0"); err == nil {
|
|
t.Fatal("VerifyBinary(substring-style mismatch) expected error, got nil")
|
|
}
|
|
if err := New().VerifyBinary("12.1.0"); err == nil {
|
|
t.Fatal("VerifyBinary(prefix-style mismatch) expected error, got nil")
|
|
}
|
|
}
|
|
|
|
func TestVerifyBinaryLookPathNotFound(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
mock := &lookPathMock{result: "", resultErr: fmt.Errorf("not found")}
|
|
mock.install("lark-cli")
|
|
t.Cleanup(mock.restore)
|
|
|
|
oldFS := vfs.DefaultFS
|
|
t.Cleanup(func() { vfs.DefaultFS = oldFS })
|
|
// Without this, VerifyBinary would fall back to the real test binary, which
|
|
// is not a lark-cli --version implementation.
|
|
vfs.DefaultFS = executableTestFS{exe: filepath.Join(t.TempDir(), "missing-lark-cli")}
|
|
|
|
if err := New().VerifyBinary("2.0.0"); err == nil {
|
|
t.Fatal("VerifyBinary(not-found) expected error, got nil")
|
|
}
|
|
}
|
|
|
|
func TestVerifyBinaryFallbackExecutableWhenNotOnPath(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("uses a POSIX shell script")
|
|
}
|
|
|
|
dir := t.TempDir()
|
|
bin := filepath.Join(dir, "lark-cli-abs")
|
|
script := "#!/bin/sh\nif [ \"$1\" = \"--version\" ]; then echo \"lark-cli version 2.1.0\"; exit 0; fi\nexit 12\n"
|
|
if err := os.WriteFile(bin, []byte(script), 0o755); err != nil {
|
|
t.Fatalf("write test binary: %v", err)
|
|
}
|
|
|
|
mock := &lookPathMock{result: "", resultErr: fmt.Errorf("not on PATH")}
|
|
mock.install("lark-cli")
|
|
t.Cleanup(mock.restore)
|
|
|
|
oldFS := vfs.DefaultFS
|
|
t.Cleanup(func() { vfs.DefaultFS = oldFS })
|
|
vfs.DefaultFS = executableTestFS{exe: bin}
|
|
|
|
if err := New().VerifyBinary("2.1.0"); err != nil {
|
|
t.Fatalf("VerifyBinary(fallback executable) error = %v, want nil", err)
|
|
}
|
|
}
|
|
|
|
func TestVerifyBinaryEmptyOutput(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("uses a POSIX shell script")
|
|
}
|
|
|
|
dir := t.TempDir()
|
|
bin := filepath.Join(dir, "lark-cli")
|
|
script := "#!/bin/sh\necho\nexit 0\n"
|
|
if err := os.WriteFile(bin, []byte(script), 0755); err != nil {
|
|
t.Fatalf("write test binary: %v", err)
|
|
}
|
|
|
|
mock := &lookPathMock{result: bin}
|
|
mock.install("lark-cli")
|
|
t.Cleanup(mock.restore)
|
|
|
|
if err := New().VerifyBinary("2.0.0"); err == nil {
|
|
t.Fatal("VerifyBinary(empty output) expected error, got nil")
|
|
}
|
|
}
|
|
|
|
func TestSkillsCommandsUseExpectedArgs(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
run func(*Updater) *NpmResult
|
|
want string
|
|
}{
|
|
{
|
|
name: "list official primary",
|
|
run: func(u *Updater) *NpmResult {
|
|
return u.runSkillsListOfficial("https://open.feishu.cn")
|
|
},
|
|
want: "-y skills add https://open.feishu.cn --list",
|
|
},
|
|
{
|
|
name: "list global",
|
|
run: func(u *Updater) *NpmResult {
|
|
return u.runSkillsListGlobal()
|
|
},
|
|
want: "-y skills ls -g",
|
|
},
|
|
{
|
|
name: "list global json",
|
|
run: func(u *Updater) *NpmResult {
|
|
return u.ListGlobalSkillsJSON()
|
|
},
|
|
want: "-y skills ls -g --json",
|
|
},
|
|
{
|
|
name: "install skill primary",
|
|
run: func(u *Updater) *NpmResult {
|
|
return u.runSkillsInstall("https://open.feishu.cn", []string{"lark-mail"})
|
|
},
|
|
want: "-y skills add https://open.feishu.cn -s lark-mail -g -y",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("uses a POSIX shell script")
|
|
}
|
|
dir := t.TempDir()
|
|
script := filepath.Join(dir, "npx")
|
|
logPath := filepath.Join(dir, "npx.log")
|
|
if err := os.WriteFile(script, []byte("#!/bin/sh\nprintf '%s\\n' \"$*\" >> \""+logPath+"\"\nexit 0\n"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))
|
|
|
|
result := tt.run(New())
|
|
if result.Err != nil {
|
|
t.Fatalf("command err = %v, want nil", result.Err)
|
|
}
|
|
raw, err := os.ReadFile(logPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.TrimSpace(string(raw)) != tt.want {
|
|
t.Fatalf("args = %q, want %q", strings.TrimSpace(string(raw)), tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestListOfficialSkillsIndexSuccess(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
fmt.Fprint(w, `{"skills":[{"name":"lark-calendar"}]}`)
|
|
}))
|
|
defer server.Close()
|
|
|
|
oldURL := officialSkillsIndexURL
|
|
officialSkillsIndexURL = server.URL
|
|
t.Cleanup(func() { officialSkillsIndexURL = oldURL })
|
|
|
|
result := New().ListOfficialSkillsIndex()
|
|
if result.Err != nil {
|
|
t.Fatalf("ListOfficialSkillsIndex() err = %v, want nil", result.Err)
|
|
}
|
|
if got := result.Stdout.String(); !strings.Contains(got, "lark-calendar") {
|
|
t.Fatalf("ListOfficialSkillsIndex() stdout = %q, want skill JSON", got)
|
|
}
|
|
}
|
|
|
|
func TestListOfficialSkillsIndexHTTPError(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
http.Error(w, "not found", http.StatusNotFound)
|
|
}))
|
|
defer server.Close()
|
|
|
|
oldURL := officialSkillsIndexURL
|
|
officialSkillsIndexURL = server.URL
|
|
t.Cleanup(func() { officialSkillsIndexURL = oldURL })
|
|
|
|
result := New().ListOfficialSkillsIndex()
|
|
if result.Err == nil || !strings.Contains(result.Err.Error(), "HTTP 404") {
|
|
t.Fatalf("ListOfficialSkillsIndex() err = %v, want HTTP 404", result.Err)
|
|
}
|
|
}
|
|
|
|
func TestListOfficialSkillsIndexBodyTooLarge(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
fmt.Fprint(w, strings.Repeat("x", skillsIndexMaxBodySize+1))
|
|
}))
|
|
defer server.Close()
|
|
|
|
oldURL := officialSkillsIndexURL
|
|
officialSkillsIndexURL = server.URL
|
|
t.Cleanup(func() { officialSkillsIndexURL = oldURL })
|
|
|
|
result := New().ListOfficialSkillsIndex()
|
|
if result.Err == nil || !strings.Contains(result.Err.Error(), "exceeds") {
|
|
t.Fatalf("ListOfficialSkillsIndex() err = %v, want exceeds", result.Err)
|
|
}
|
|
if result.Stdout.Len() != 0 {
|
|
t.Fatalf("ListOfficialSkillsIndex() stdout len = %d, want 0", result.Stdout.Len())
|
|
}
|
|
}
|
|
|
|
func TestListOfficialSkillsIndexTimeout(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
time.Sleep(200 * time.Millisecond)
|
|
fmt.Fprint(w, `{"skills":[{"name":"lark-calendar"}]}`)
|
|
}))
|
|
defer server.Close()
|
|
|
|
oldURL := officialSkillsIndexURL
|
|
oldTimeout := skillsIndexFetchTimeout
|
|
officialSkillsIndexURL = server.URL
|
|
skillsIndexFetchTimeout = 50 * time.Millisecond
|
|
t.Cleanup(func() {
|
|
officialSkillsIndexURL = oldURL
|
|
skillsIndexFetchTimeout = oldTimeout
|
|
})
|
|
|
|
result := New().ListOfficialSkillsIndex()
|
|
var netErr net.Error
|
|
if result.Err == nil || (!errors.Is(result.Err, context.DeadlineExceeded) && !(errors.As(result.Err, &netErr) && netErr.Timeout())) {
|
|
t.Fatalf("ListOfficialSkillsIndex() err = %v, want timeout error", result.Err)
|
|
}
|
|
}
|
|
|
|
func TestListOfficialSkillsIndexRejectsNonHTTPSRedirect(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
http.Redirect(w, r, "http://example.com/skills.json", http.StatusFound)
|
|
}))
|
|
defer server.Close()
|
|
|
|
oldURL := officialSkillsIndexURL
|
|
officialSkillsIndexURL = server.URL
|
|
t.Cleanup(func() { officialSkillsIndexURL = oldURL })
|
|
|
|
result := New().ListOfficialSkillsIndex()
|
|
if result.Err == nil || !strings.Contains(result.Err.Error(), "non-HTTPS") {
|
|
t.Fatalf("ListOfficialSkillsIndex() err = %v, want non-HTTPS redirect", result.Err)
|
|
}
|
|
}
|
|
|
|
func TestListOfficialSkillsIndexUsesOverride(t *testing.T) {
|
|
result := (&Updater{SkillsIndexFetchOverride: func() *NpmResult {
|
|
r := &NpmResult{}
|
|
r.Stdout.WriteString(`{"skills":[{"name":"override-skill"}]}`)
|
|
return r
|
|
}}).ListOfficialSkillsIndex()
|
|
if result.Err != nil {
|
|
t.Fatalf("ListOfficialSkillsIndex() err = %v, want nil", result.Err)
|
|
}
|
|
if !strings.Contains(result.Stdout.String(), "override-skill") {
|
|
t.Fatalf("ListOfficialSkillsIndex() stdout = %q, want override result", result.Stdout.String())
|
|
}
|
|
}
|
|
|
|
func TestListOfficialSkillsFallsBack(t *testing.T) {
|
|
called := []string{}
|
|
updater := &Updater{
|
|
SkillsCommandOverride: func(args ...string) *NpmResult {
|
|
called = append(called, strings.Join(args, " "))
|
|
r := &NpmResult{}
|
|
if strings.Contains(strings.Join(args, " "), "https://open.feishu.cn") {
|
|
r.Err = fmt.Errorf("primary failed")
|
|
return r
|
|
}
|
|
r.Stdout.WriteString("lark-calendar\n")
|
|
return r
|
|
},
|
|
}
|
|
|
|
result := updater.ListOfficialSkills()
|
|
if result.Err != nil {
|
|
t.Fatalf("ListOfficialSkills() err = %v, want nil", result.Err)
|
|
}
|
|
if len(called) != 2 {
|
|
t.Fatalf("called %d commands, want 2: %#v", len(called), called)
|
|
}
|
|
if !strings.Contains(called[1], "larksuite/cli --list") {
|
|
t.Fatalf("fallback call = %q, want larksuite/cli --list", called[1])
|
|
}
|
|
}
|
|
|
|
func TestContainsPnpmMarker(t *testing.T) {
|
|
cases := []struct {
|
|
path string
|
|
want bool
|
|
}{
|
|
// Classic virtual-store layout (.pnpm segment).
|
|
{"/Users/x/Library/pnpm/global/5/node_modules/.pnpm/@larksuite+cli@1.0.44/node_modules/@larksuite/cli/bin/lark-cli", true},
|
|
{`C:\Users\x\AppData\Local\pnpm\global\5\node_modules\.pnpm\@larksuite+cli@1.0.44\node_modules\@larksuite\cli\bin\lark-cli.exe`, true},
|
|
// Global content-addressable store layout (pnpm 11): resolved path runs
|
|
// through the pnpm home store, a "pnpm" segment with no ".pnpm".
|
|
{"/Users/x/Library/pnpm/store/v11/links/@larksuite/cli/1.0.59/abc123/node_modules/@larksuite/cli/bin/lark-cli", true},
|
|
{"/home/x/.local/share/pnpm/store/v10/@larksuite/cli/node_modules/@larksuite/cli/bin/lark-cli", true},
|
|
{`C:\Users\x\AppData\Local\pnpm\store\v11\links\@larksuite\cli\node_modules\@larksuite\cli\bin\lark-cli.exe`, true},
|
|
// npm and non-package installs — no pnpm/.pnpm segment.
|
|
{"/usr/local/lib/node_modules/@larksuite/cli/bin/lark-cli", false},
|
|
{"/usr/local/bin/lark-cli", false},
|
|
// Substrings that must NOT match: segment must be exactly .pnpm, or
|
|
// "pnpm" immediately followed by "store".
|
|
{"/opt/homebrew/.pnpmfoo/node_modules/@larksuite/cli/bin/lark-cli", false},
|
|
{"/opt/pnpmfoo/node_modules/@larksuite/cli/bin/lark-cli", false},
|
|
// A bare "pnpm" directory NOT followed by "store" (e.g. an npm install
|
|
// living under a dir named pnpm) must not be misclassified as pnpm.
|
|
{"/opt/pnpm/lib/node_modules/@larksuite/cli/bin/lark-cli", false},
|
|
}
|
|
for _, c := range cases {
|
|
if got := containsPnpmMarker(c.path); got != c.want {
|
|
t.Errorf("containsPnpmMarker(%q) = %v, want %v", c.path, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDetectInstallMethod_Pnpm(t *testing.T) {
|
|
u := &Updater{DetectOverride: nil}
|
|
u.DetectOverride = func() DetectResult {
|
|
// Exercise the real classification by feeding a resolved path via a small shim.
|
|
return detectFromResolved("/x/node_modules/.pnpm/@larksuite+cli@1.0.44/node_modules/@larksuite/cli/bin/lark-cli", true, true)
|
|
}
|
|
got := u.DetectInstallMethod()
|
|
if got.Method != InstallPnpm {
|
|
t.Errorf("Method = %v, want InstallPnpm", got.Method)
|
|
}
|
|
if !got.PnpmAvailable {
|
|
t.Errorf("PnpmAvailable = false, want true")
|
|
}
|
|
}
|
|
|
|
func TestDetectInstallMethod_NpmVsManual(t *testing.T) {
|
|
if m := detectFromResolved("/usr/local/lib/node_modules/@larksuite/cli/bin/lark-cli", true, false).Method; m != InstallNpm {
|
|
t.Errorf("npm path Method = %v, want InstallNpm", m)
|
|
}
|
|
if m := detectFromResolved("/usr/local/bin/lark-cli", false, false).Method; m != InstallManual {
|
|
t.Errorf("manual path Method = %v, want InstallManual", m)
|
|
}
|
|
}
|
|
|
|
func TestCanAutoUpdate_Pnpm(t *testing.T) {
|
|
if !(DetectResult{Method: InstallPnpm, PnpmAvailable: true}).CanAutoUpdate() {
|
|
t.Error("pnpm available should CanAutoUpdate")
|
|
}
|
|
if (DetectResult{Method: InstallPnpm, PnpmAvailable: false}).CanAutoUpdate() {
|
|
t.Error("pnpm unavailable should not CanAutoUpdate")
|
|
}
|
|
}
|
|
|
|
func TestManualReason_Pnpm(t *testing.T) {
|
|
if got := (DetectResult{Method: InstallPnpm, NpmAvailable: false, PnpmAvailable: false}).ManualReason(); got != "installed via pnpm, but pnpm is not available in PATH" {
|
|
t.Errorf("pnpm reason = %q", got)
|
|
}
|
|
if got := (DetectResult{Method: InstallManual}).ManualReason(); got != "not installed via npm or pnpm" {
|
|
t.Errorf("manual reason = %q", got)
|
|
}
|
|
}
|
|
|
|
func TestRunPnpmInstall_Override(t *testing.T) {
|
|
u := &Updater{PnpmInstallOverride: func(version string) *NpmResult {
|
|
r := &NpmResult{}
|
|
r.Stdout.WriteString("added @larksuite/cli@" + version)
|
|
return r
|
|
}}
|
|
got := u.RunPnpmInstall("2.0.0")
|
|
if got.Err != nil {
|
|
t.Fatalf("unexpected err: %v", got.Err)
|
|
}
|
|
if !strings.Contains(got.CombinedOutput(), "2.0.0") {
|
|
t.Errorf("output = %q, want version echoed", got.CombinedOutput())
|
|
}
|
|
}
|
|
|
|
func TestRunPnpmInstall_Error(t *testing.T) {
|
|
wantErr := errors.New("boom")
|
|
u := &Updater{PnpmInstallOverride: func(string) *NpmResult { return &NpmResult{Err: wantErr} }}
|
|
if got := u.RunPnpmInstall("2.0.0"); !errors.Is(got.Err, wantErr) {
|
|
t.Errorf("err = %v, want %v", got.Err, wantErr)
|
|
}
|
|
}
|
|
|
|
func TestSkillsInvocation(t *testing.T) {
|
|
addArgs := []string{"-y", "skills", "add", "https://open.feishu.cn", "-g", "-y"}
|
|
cases := []struct {
|
|
name string
|
|
method InstallMethod
|
|
pnpmAvailable bool
|
|
args []string
|
|
wantLauncher string
|
|
wantRest []string
|
|
}{
|
|
{"pnpm install + pnpm available → pnpm dlx, drop leading -y", InstallPnpm, true, addArgs,
|
|
"pnpm", []string{"dlx", "skills", "add", "https://open.feishu.cn", "-g", "-y"}},
|
|
{"pnpm install but pnpm unavailable → npx unchanged", InstallPnpm, false, addArgs,
|
|
"npx", addArgs},
|
|
{"npm install → npx unchanged", InstallNpm, false, addArgs,
|
|
"npx", addArgs},
|
|
{"manual install → npx unchanged", InstallManual, false, []string{"-y", "skills", "ls", "-g"},
|
|
"npx", []string{"-y", "skills", "ls", "-g"}},
|
|
{"pnpm without a leading -y → prepend dlx only", InstallPnpm, true, []string{"skills", "ls", "-g"},
|
|
"pnpm", []string{"dlx", "skills", "ls", "-g"}},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
gotLauncher, gotRest := skillsInvocation(c.method, c.pnpmAvailable, c.args)
|
|
if gotLauncher != c.wantLauncher {
|
|
t.Errorf("launcher = %q, want %q", gotLauncher, c.wantLauncher)
|
|
}
|
|
if strings.Join(gotRest, " ") != strings.Join(c.wantRest, " ") {
|
|
t.Errorf("rest = %v, want %v", gotRest, c.wantRest)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestDetectInstallMethod_Caches locks the fix for the post-update re-detection
|
|
// hazard: DetectInstallMethod must return the first (pre-update) detection on
|
|
// subsequent calls, so the skills launcher chosen after the binary is replaced
|
|
// stays consistent with what was detected — and reported — before the update.
|
|
func TestDetectInstallMethod_Caches(t *testing.T) {
|
|
u := New()
|
|
cached := DetectResult{Method: InstallPnpm, PnpmAvailable: true, ResolvedPath: "/x/pnpm/store/v11/links/@larksuite/cli/1.0.0/node_modules/@larksuite/cli/bin/lark-cli"}
|
|
u.detectCache = &cached
|
|
got := u.DetectInstallMethod()
|
|
if got.Method != InstallPnpm || !got.PnpmAvailable {
|
|
t.Errorf("expected cached pnpm result to be returned, got %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestSkillsBrandHosts(t *testing.T) {
|
|
cases := []struct {
|
|
brand core.LarkBrand
|
|
wantIndex string
|
|
wantSource string
|
|
}{
|
|
{core.BrandFeishu, "https://open.feishu.cn/.well-known/skills/index.json", "https://open.feishu.cn"},
|
|
{core.BrandLark, "https://open.larksuite.com/.well-known/skills/index.json", "https://open.larksuite.com"},
|
|
}
|
|
for _, c := range cases {
|
|
u := &Updater{Brand: c.brand}
|
|
if got := u.skillsIndexURL(); got != c.wantIndex {
|
|
t.Errorf("brand %q: skillsIndexURL = %q, want %q", c.brand, got, c.wantIndex)
|
|
}
|
|
if got := u.skillsSource(); got != c.wantSource {
|
|
t.Errorf("brand %q: skillsSource = %q, want %q", c.brand, got, c.wantSource)
|
|
}
|
|
}
|
|
}
|